19 Commits

Author SHA1 Message Date
lfox
35526331fa fixed permissions issues with deploy watch target 2026-11-01 21:47:41 +00:00
cdc37851cd blog: update posts 2026-11-01 21:47:16 +00:00
3e161eb00e blog: update posts 2026-11-01 21:42:29 +00:00
a93868d885 blog: update posts 2026-11-01 21:36:11 +00:00
6dfc113975 blog: update posts 2026-11-01 21:30:37 +00:00
386c17b496 blog: update posts 2026-11-01 21:28:28 +00:00
0a4ea99e1a blog: update posts 2026-11-01 21:22:36 +00:00
9c78ff2d25 blog: update posts 2026-11-01 21:18:43 +00:00
5c366a2137 blog: update posts 2026-11-01 21:08:13 +00:00
7b67dd68f0 blog: update posts 2026-11-01 21:06:03 +00:00
lfox
2c5b403522 added DEPLOY_TRIGGER_DIR to possible .env vars, updated compose 2026-11-01 21:04:48 +00:00
76d0260a52 blog: update posts 2026-11-01 21:02:53 +00:00
debfc217ba blog: update posts 2026-11-01 20:59:59 +00:00
a0b3ac7c0d blog: update posts 2026-11-01 20:03:40 +00:00
lfox
31c1303a0f updated deploy scripts 2026-11-01 20:02:17 +00:00
df2041ba67 blog: update posts 2026-11-01 20:01:21 +00:00
Hunter
51e5ef9aee added .env.example 2026-11-01 13:29:53 -05:00
Hunter
8796682bb7 moved blog posts out of content.ts for external management 2026-11-01 11:14:38 -05:00
Hunter
ef1cc9144d fixed small typo in import 2026-11-01 01:47:41 -04:00
11 changed files with 42 additions and 80 deletions

1
.env.example Normal file
View File

@@ -0,0 +1 @@
DEPLOY_TRIGGER_SECRET=#A random secret string used to trigger the live site's redeploy after a successful push from blog-manager. Must match DEPLOY_TRIGGER_SECRET in blog-manager/.env.

2
.gitignore vendored
View File

@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
.env
# vercel
.vercel

View File

@@ -9,6 +9,7 @@ FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
@@ -22,7 +23,7 @@ ENV HOSTNAME=0.0.0.0
ENV PORT=3050
RUN addgroup --system --gid 1001 app && \
adduser --system --uid 1001 app
adduser --system --uid 1001 -G app app
COPY --from=build /app/public ./public
COPY --from=build --chown=app:app /app/.next/standalone ./

View File

@@ -9,7 +9,7 @@ import BlogList from "@/components/BlogList";
* links use the ?category= form so navigation stays consistent across both.
* Unknown slugs 404: a real route can notFound(), a query param cannot.
*/
export default function BlogCategoryPage({ params }: { params: Promise<{ category: string }> }) {
export default async function BlogCategoryPage({ params }: { params: Promise<{ category: string }> }) {
const { category } = await params;
if (!category || !blogCategories().includes(category)) notFound();

View File

@@ -1,6 +1,7 @@
"use client";
import { Suspense, useSearchParams } from "react";
import { Suspense } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import {
POSTS_PER_PAGE,

29
data/blog-posts.json Normal file
View File

@@ -0,0 +1,29 @@
[
{
"id": 1,
"slug": "adding-a-blog-tab",
"title": "Testing testing 123 12134",
"description": "A portfolio is a static-ish site with a lot of moving parts. Here's how I added a fourth tab to this very site without adding a single dependency.",
"date": "2026-10-26",
"category": "development",
"tags": [
"next.js",
"react",
"typescript"
],
"details": "# Testing testing 12345\n\nThis little corner of the site started as a question: **how does long-form content fit into a site that was built for short-form stuff?** Portfolio sites are grids of cards, and a blog is a column of words. The answer turned out to be \"don't treat them differently at all.\"\n\n<blockquote>\nA blog is just another collection of cards. The difference is what happens when you click one.\n</blockquote>\n\n## The shape of the problem\n\nEvery tab on this site follows the same recipe:\n\n- A typed data array in a single content file, which acts as the whole CMS\n- A shared layout that provides the nav chrome and the footer\n- A thin client page that renders one list component\n- A detail page that reuses the same layout, minus the nav, plus a context provider\n\nA post that fits that shape is just another entry in an array. A post that doesn't fit it is a content management system in disguise, and that's a whole other project.\n\n## Why the markdown was already there\n\nThe project detail pages render markdown through a small remark pipeline that has been sitting in the codebase for a while, used by the project pages. The blog needed exactly the same thing, which meant the data layer for a blog post is mostly just a type and a lookup function. The pipeline is shared, so a post and a project detail are rendered by the same code, with the same quirks, and the same fixes.\n\nThat is the whole trick. The feature felt like it would need infrastructure, but the infrastructure already existed for a different feature. When a new feature turns out to be a clone of an old one, the new feature is cheap, and the codebase gets a little more regular as a side effect.\n\n## What actually changed\n\nA new type, a new data array, a new lookup function, a new tab in the nav, two layouts, a list page, a card, and a detail page. No new dependencies, no new build steps, no new configuration. The tab order now reads profile, project, blog, mods, and the blog sits in the middle of the two portfolio tabs.\n\nEditing a post from here on out is editing the array, which is the same as editing a project. That's the part I wanted.\n"
},
{
"id": 2,
"slug": "hello-world",
"title": "Hello, world. This is the new blog.",
"description": "A short note about why this site grew a fourth tab, how the posts are written, and what the rest of this page demonstrates.",
"date": "2026-09-14",
"category": "meta",
"tags": [
"meta",
"blog"
],
"details": "# Hello, world. This is the new blog.\n\nThis is a blog now. There's a fourth tab up top, and it works the same way the rest of the site works: content lives in the repository as plain data, the page renders it, and the markdown is processed by the same pipeline that renders the project detail pages.\n\n<blockquote>\nIf a site is only ever going to have three tabs, it's a portfolio. The fourth tab is where it starts being a site.\n</blockquote>\n\n## Why it exists\n\nThe portfolio is for finished things, and the mods page is for the things I ship to other people's games. This is for the middle: write-ups, notes, and the occasional \"here's how I did that\" that doesn't belong anywhere else. It's also the first place on this site where I write in the first person at length, which is a change.\n\n## How posts are written\n\nEach post is a typed object in the content file, with a slug, a date, a short description for the card, and a markdown body. The body goes through the site's existing remark pipeline, so any markdown that works on the project pages works here, including raw HTML blocks when I want a specific layout.\n\n- Headings render in the mono face, like the rest of the site\n- Blockquotes get the raised, bordered treatment you can see above\n- Images are kept out of this post on purpose. The pipeline supports them, and a future post will use one\n\n## What this page is doing while you read it\n\nThis page is a dynamic route. The slug comes from the URL, the layout resolves it to rendered HTML on the server, and the page on the client side reads that and paints it in. That's the same architecture the project detail pages use, which is the point of reusing it: a new page type that can't do anything the old page types can't already do.\n"
}
]

View File

@@ -1,3 +1,5 @@
import blogPosts from "./blog-posts.json";
export type MoreSection = {
title?: string;
@@ -221,85 +223,13 @@ A real-world solution for event staff and volunteer management.
}];
export const BLOG_POSTS: BlogPost[] = [
{
id: 1,
slug: "adding-a-blog-tab",
title: "Adding a blog tab to a portfolio site",
description: "A portfolio is a static-ish site with a lot of moving parts. Here's how I added a fourth tab to this very site without adding a single dependency.",
date: "2026-10-26",
category: "development",
tags: ["next.js", "react", "typescript"],
details: `# Adding a blog tab to a portfolio site
This little corner of the site started as a question: **how does long-form content fit into a site that was built for short-form stuff?** Portfolio sites are grids of cards, and a blog is a column of words. The answer turned out to be \"don't treat them differently at all.\"
<blockquote>
A blog is just another collection of cards. The difference is what happens when you click one.
</blockquote>
## The shape of the problem
Every tab on this site follows the same recipe:
- A typed data array in a single content file, which acts as the whole CMS
- A shared layout that provides the nav chrome and the footer
- A thin client page that renders one list component
- A detail page that reuses the same layout, minus the nav, plus a context provider
A post that fits that shape is just another entry in an array. A post that doesn't fit it is a content management system in disguise, and that's a whole other project.
## Why the markdown was already there
The project detail pages render markdown through a small remark pipeline that has been sitting in the codebase for a while, used by the project pages. The blog needed exactly the same thing, which meant the data layer for a blog post is mostly just a type and a lookup function. The pipeline is shared, so a post and a project detail are rendered by the same code, with the same quirks, and the same fixes.
That is the whole trick. The feature felt like it would need infrastructure, but the infrastructure already existed for a different feature. When a new feature turns out to be a clone of an old one, the new feature is cheap, and the codebase gets a little more regular as a side effect.
## What actually changed
A new type, a new data array, a new lookup function, a new tab in the nav, two layouts, a list page, a card, and a detail page. No new dependencies, no new build steps, no new configuration. The tab order now reads profile, project, blog, mods, and the blog sits in the middle of the two portfolio tabs.
Editing a post from here on out is editing the array, which is the same as editing a project. That's the part I wanted.
`,
},
{
id: 2,
slug: "hello-world",
title: "Hello, world. This is the new blog.",
description: "A short note about why this site grew a fourth tab, how the posts are written, and what the rest of this page demonstrates.",
date: "2026-09-14",
category: "meta",
tags: ["meta", "blog"],
details: `# Hello, world. This is the new blog.
This is a blog now. There's a fourth tab up top, and it works the same way the rest of the site works: content lives in the repository as plain data, the page renders it, and the markdown is processed by the same pipeline that renders the project detail pages.
<blockquote>
If a site is only ever going to have three tabs, it's a portfolio. The fourth tab is where it starts being a site.
</blockquote>
## Why it exists
The portfolio is for finished things, and the mods page is for the things I ship to other people's games. This is for the middle: write-ups, notes, and the occasional \"here's how I did that\" that doesn't belong anywhere else. It's also the first place on this site where I write in the first person at length, which is a change.
## How posts are written
Each post is a typed object in the content file, with a slug, a date, a short description for the card, and a markdown body. The body goes through the site's existing remark pipeline, so any markdown that works on the project pages works here, including raw HTML blocks when I want a specific layout.
- Headings render in the mono face, like the rest of the site
- Blockquotes get the raised, bordered treatment you can see above
- Images are kept out of this post on purpose. The pipeline supports them, and a future post will use one
## What this page is doing while you read it
This page is a dynamic route. The slug comes from the URL, the layout resolves it to rendered HTML on the server, and the page on the client side reads that and paints it in. That's the same architecture the project detail pages use, which is the point of reusing it: a new page type that can't do anything the old page types can't already do.
`,
}
];
/* Blog posts live in blog-posts.json so tooling (blog-manager/) can edit them
* without rewriting TypeScript. The shape must match the BlogPost type above. */
export const BLOG_POSTS: BlogPost[] = blogPosts as BlogPost[];
/* ---- blog directory helpers ----
* Pagination and category filtering for the /blog list. Everything derives
* from BLOG_POSTS above, which remains the single source of truth. */
* from BLOG_POSTS (data/blog-posts.json), the single source of truth. */
/** Small enough that pagination is visible as soon as a few posts exist. */
export const POSTS_PER_PAGE = 3;

0
scripts/deploy-watch.service Normal file → Executable file
View File

0
scripts/deploy-watch.sh Normal file → Executable file
View File

0
scripts/install-deploy-watch.sh Normal file → Executable file
View File

0
scripts/uninstall-deploy-watch.sh Normal file → Executable file
View File