Add blog CMS with view tracking #1
@@ -17,108 +17,3 @@ CREATE TABLE "BlogPost" (
|
|||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "BlogPost_slug_key" ON "BlogPost"("slug");
|
CREATE UNIQUE INDEX "BlogPost_slug_key" ON "BlogPost"("slug");
|
||||||
|
|
||||||
-- Seed starter posts
|
|
||||||
INSERT INTO "BlogPost" ("id", "title", "slug", "excerpt", "coverImageUrl", "content", "isPublished", "views", "publishedAt", "createdAt", "updatedAt")
|
|
||||||
VALUES
|
|
||||||
(
|
|
||||||
'blog_bootstrapping_luna',
|
|
||||||
'Bootstrapping Luna on Home Infra',
|
|
||||||
'bootstrapping-luna-on-home-infra',
|
|
||||||
'How I wired a local-first assistant onto my own infrastructure without making the whole stack brittle.',
|
|
||||||
NULL,
|
|
||||||
$$# Bootstrapping Luna on Home Infra
|
|
||||||
|
|
||||||
Running an assistant from your own box sounds fun until the first reboot eats half the state and the other half lives in five unrelated scripts.
|
|
||||||
|
|
||||||
## The actual goal
|
|
||||||
|
|
||||||
I wanted a setup that stayed **local-first**, reacted quickly, and did not fall apart the second a service restarted. That meant a few rules:
|
|
||||||
|
|
||||||
- keep the state in places I control
|
|
||||||
- make tools callable without browser hacks
|
|
||||||
- prefer plain files and tiny APIs over magic
|
|
||||||
|
|
||||||
## What worked
|
|
||||||
|
|
||||||
The best decision was treating memory as part of the product instead of an afterthought. Notes, small docs, and project context live close to the runtime, so the assistant does not have to guess everything from chat history.
|
|
||||||
|
|
||||||
## What still sucks
|
|
||||||
|
|
||||||
The annoying bits are always the same: auth edges, stale sessions, and making sure background jobs stay visible instead of silently drifting.
|
|
||||||
|
|
||||||
## The payoff
|
|
||||||
|
|
||||||
Once the boring plumbing was stable, the fun part happened: the assistant could actually help with real work instead of just answering one-off questions.
|
|
||||||
$$,
|
|
||||||
true,
|
|
||||||
182,
|
|
||||||
TIMESTAMP '2026-07-12 18:30:00',
|
|
||||||
CURRENT_TIMESTAMP,
|
|
||||||
CURRENT_TIMESTAMP
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'blog_prisma_content_systems',
|
|
||||||
'What Prisma Gets Right for Small Content Systems',
|
|
||||||
'what-prisma-gets-right-for-small-content-systems',
|
|
||||||
'Why a tiny Prisma-backed CMS is often cleaner than bolting markdown files onto a growing product.',
|
|
||||||
NULL,
|
|
||||||
$$# What Prisma Gets Right for Small Content Systems
|
|
||||||
|
|
||||||
When a site starts growing beyond a landing page, content sneaks in everywhere. Suddenly there are projects, changelogs, notes, announcements, and blog posts.
|
|
||||||
|
|
||||||
## Why I keep reaching for Prisma
|
|
||||||
|
|
||||||
Prisma gives me a couple things I care about:
|
|
||||||
|
|
||||||
1. typed data access
|
|
||||||
2. migrations I can inspect
|
|
||||||
3. one place to validate how content actually looks
|
|
||||||
|
|
||||||
That matters because the problem is rarely “where do I put the text?” The problem is keeping **editing**, **rendering**, and **publishing** from drifting apart.
|
|
||||||
|
|
||||||
## Markdown still wins
|
|
||||||
|
|
||||||
I still like markdown for the authoring format. It is fast, portable, and not weird to diff. The database just becomes the durable source of truth around that markdown.
|
|
||||||
|
|
||||||
## The tradeoff
|
|
||||||
|
|
||||||
You do lose some git-native niceness compared to file-based content. But once non-technical editing or richer admin flows matter, the database route gets way less annoying.
|
|
||||||
$$,
|
|
||||||
true,
|
|
||||||
141,
|
|
||||||
TIMESTAMP '2026-07-09 16:45:00',
|
|
||||||
CURRENT_TIMESTAMP,
|
|
||||||
CURRENT_TIMESTAMP
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'blog_shipping_small_tools',
|
|
||||||
'Shipping Small Tools Without Making a Mess',
|
|
||||||
'shipping-small-tools-without-making-a-mess',
|
|
||||||
'A few rules I use to keep side tools useful, maintainable, and not instantly cursed.',
|
|
||||||
NULL,
|
|
||||||
$$# Shipping Small Tools Without Making a Mess
|
|
||||||
|
|
||||||
Tiny tools are dangerous because they feel too small to design properly. That is how you end up with “temporary” scripts haunting production six months later.
|
|
||||||
|
|
||||||
## My rule set
|
|
||||||
|
|
||||||
- name things like they will still exist next month
|
|
||||||
- write down the weird assumptions
|
|
||||||
- keep input and output dead simple
|
|
||||||
- make deletion easier than expansion
|
|
||||||
|
|
||||||
## The hidden trick
|
|
||||||
|
|
||||||
A good small tool usually does **less** than you first wanted. The moment a script starts collecting flags, modes, and special cases, it is begging to become a real service.
|
|
||||||
|
|
||||||
## Final thought
|
|
||||||
|
|
||||||
Shipping fast is good. Shipping something you can still understand later is better.
|
|
||||||
$$,
|
|
||||||
true,
|
|
||||||
96,
|
|
||||||
TIMESTAMP '2026-07-05 10:20:00',
|
|
||||||
CURRENT_TIMESTAMP,
|
|
||||||
CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export default async function BlogsPage() {
|
|||||||
writing
|
writing
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mt-4 max-w-3xl text-4xl font-semibold text-white sm:text-5xl">
|
<h1 className="mt-4 max-w-3xl text-4xl font-semibold text-white sm:text-5xl">
|
||||||
Build notes, infra thoughts, and the occasional strongly held opinion.
|
Technical writing on infrastructure, software, and product work.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 max-w-2xl text-sm leading-7 text-gray-400 sm:text-base">
|
<p className="mt-4 max-w-2xl text-sm leading-7 text-gray-400 sm:text-base">
|
||||||
Posts live in the same database-backed admin system as the rest of the site, with markdown
|
Articles are managed through the same admin system as the rest of the site, with markdown
|
||||||
authoring and public view tracking.
|
authoring and publishing controls.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,24 @@ export function MarkdownContent({ content }: { content: string }) {
|
|||||||
blockquote: ({ children }) => (
|
blockquote: ({ children }) => (
|
||||||
<blockquote className="border-l-2 border-white/20 pl-4 italic text-gray-400">{children}</blockquote>
|
<blockquote className="border-l-2 border-white/20 pl-4 italic text-gray-400">{children}</blockquote>
|
||||||
),
|
),
|
||||||
|
img: ({ src, alt }) => (
|
||||||
|
// Markdown images should feel deliberate in the article layout.
|
||||||
|
<img
|
||||||
|
src={src || ""}
|
||||||
|
alt={alt || ""}
|
||||||
|
className="my-6 w-full rounded-2xl border border-white/10 bg-black/40 object-cover"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="my-6 overflow-x-auto rounded-2xl border border-white/10">
|
||||||
|
<table className="min-w-full border-collapse text-left text-sm">{children}</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => <thead className="bg-white/6 text-white">{children}</thead>,
|
||||||
|
tbody: ({ children }) => <tbody className="divide-y divide-white/10">{children}</tbody>,
|
||||||
|
tr: ({ children }) => <tr className="divide-x divide-white/10">{children}</tr>,
|
||||||
|
th: ({ children }) => <th className="px-4 py-3 font-semibold">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="px-4 py-3 align-top text-gray-300">{children}</td>,
|
||||||
code: ({ className, children }) => {
|
code: ({ className, children }) => {
|
||||||
const inline = !className;
|
const inline = !className;
|
||||||
if (inline) {
|
if (inline) {
|
||||||
|
|||||||
Reference in New Issue
Block a user