Personal blog built as a Cloudflare Worker. Markdown content lives under content/, is bundled at build time, and is rendered by Worker route handlers with HTMX-enhanced navigation.
content/
pages/ Static pages such as home, about, and newsletter
posts/ Blog posts, link posts, notes, drafts, and articles
public/ Static assets served by the Workers Static Assets binding
scripts/ Build and sync scripts
src/
index.ts Worker entrypoint and top-level request dispatch
manifest.ts Generated content manifest; do not edit manually
markdown.ts Frontmatter and Markdown rendering helpers
routes/ Page and admin route handlers
templates/ HTML templates and HTMX partials
lib/ Shared helpers for auth, dates, tags, sessions, etc.
styles/ Tailwind input CSS; compiled CSS is written to public/cssPost types are inferred from location/frontmatter:
content/posts/articles/**→ article- other posts with
link:→ link post - other posts without
link:→ note
Draft posts use draft: true frontmatter and are included in internal indexes but skipped from public post lists.
Requires Node.js 22.13.0 or newer.
pnpm install
pnpm run dev
pnpm run build
pnpm run deployOther useful commands:
pnpm run build:css # Compile Tailwind CSS to public/css/styles.css
pnpm run build:manifest # Regenerate src/manifest.ts and public/sitemap.xml
pnpm run typecheck
pnpm testMarkdown files are imported as text by Wrangler using the rules entry in wrangler.jsonc. The build step runs scripts/build-manifest.ts, which:
- scans
content/pages/**/*.mdandcontent/posts/**/*.md - parses frontmatter with
front-matter - normalizes post metadata, tags, dates, and links
- generates
src/manifest.ts - generates
public/sitemap.xml
At runtime, the Worker imports the generated manifest and renders pages from src/routes/pages.ts and templates under src/templates/.
The layout loads HTMX and enables boosted navigation on the page body:
<body hx-boost="true" hx-target="#content" hx-swap="innerHTML">Normal browser requests receive a full HTML document from layout(...). HTMX requests include HX-Request: true; src/index.ts detects these and returns only the content partial so page transitions replace #content without a full reload.
History restore requests are treated specially: if HX-History-Restore-Request: true is present, the Worker returns full HTML so HTMX can rebuild browser history state correctly.
The site runs as a Cloudflare Worker configured in wrangler.jsonc:
src/index.tsis the Worker entrypoint.- Static files in
public/are served through theASSETSbinding. - Markdown files are bundled as text modules at build time.
/css/,/fonts/,/games/,/images/,/js/,/robots.txt, and/sitemap.xmlare routed to static assets before dynamic routing.- Versioned CSS/images and immutable fonts/JS get long-lived cache headers.
- Dynamic routes are dispatched to admin routes first, then public page routes.
- Responses pass through shared security headers.
Deployment is:
pnpm run deploywhich runs the build and then deploys with Wrangler.
The /admin section contains protected tools for authoring link posts and notes. Authentication uses Sign in with Apple, restricted by ADMIN_EMAIL, with signed-cookie sessions and no KV dependency.
The remote MCP endpoint is https://sids.in/admin/mcp. It uses OAuth 2.1 with PKCE, Sign in with Apple for administrator authentication, an explicit client-consent screen, and separate blog:read and blog:write scopes.
Available tools:
list_tagslist_postscreate_draft_postfor draft links, notes, and articlesedit_postto selectively replace a post's title, slug, tags, or Markdown contentchange_post_statusto publish a draft or move a published post back to draft
OAuth clients, grants, and tokens use the OAUTH_KV binding declared in wrangler.jsonc. Wrangler can provision this namespace during deployment. To bind an existing namespace instead, create one explicitly:
pnpm exec wrangler kv namespace create OAUTH_KVThen add the returned namespace ID to the existing binding in wrangler.jsonc:
The MCP OAuth endpoints are /admin/oauth/authorize, /admin/oauth/token, and /admin/oauth/register.
Standalone games live under public/games/ and are served as static assets. Update public/games/index.html when adding or removing games.
Posts in content/posts/**/*.md can be synced two-way with Bear using bearcli.
Run a safe preview first:
pnpm run sync:bear:dry-runApply the sync:
pnpm run sync:bearBy default, the script will not replace the body/content portion of an existing Bear note from local files. It may still update Bear metadata such as YAML frontmatter, the Bear-only Markdown title, and managed tags. To explicitly allow local files to overwrite Bear note bodies, pass:
pnpm run sync:bear -- --overwrite-bear-contentFor an automated run, explicitly enable committing and pushing files created or updated from Bear:
pnpm run sync:bear -- --commit-and-pushThis mode retains Bear-produced file paths until publication succeeds, retrying failed commits and pending pushes on later runs.
The sync script:
- matches local posts to Bear notes using
.bear-posts-sync.json - creates missing Bear notes from local posts
- creates local Markdown files from new Bear notes tagged under
#sids.in - updates the side that changed when only one side changed
- skips syncing Bear notes tagged
#sids.in/~wipfrom Bear to local files - reports and skips conflicts when both sides changed
- archives Bear notes when their local post was deleted
- keeps Bear tags in sync from frontmatter tags as nested tags under
#sids.in - with
--commit-and-push, commits and pushes files created or updated from Bear usingchore: sync posts from Bear
Managed Bear tags:
#sids.inwhen a post has no deeper managed tag#sids.in/<tag>for each YAML frontmatter tag#sids.in/~draftfor draft posts#sids.in/~articlefor article posts and for routing new Bear-created notes tocontent/posts/articles/#sids.in/~wipfor Bear notes that must not sync to local files
New Bear-created notes should contain normal post frontmatter. Path routing is:
#sids.in/~article→content/posts/articles/YYYY-MM-slug.md- otherwise →
content/posts/YYYY/MM-DD-slug.md
If a Bear-created note has no YAML frontmatter, the sync creates frontmatter with draft: true, today’s date, empty tags/description, and a title from the leading Markdown # Title or Bear’s note title. If a note has frontmatter but a missing or empty title, the sync uses the body’s Markdown # Title or Bear’s note title. If slug is missing or empty, the sync derives one from title; if date is missing or empty, it uses today’s date. For newly imported notes, regular nested Bear tags such as #sids.in/ai are merged into YAML tags, while metadata tags beginning with ~ are excluded. Repaired frontmatter is written back to both the Bear note and the local Markdown file.
Bear inserts tag lines into note content. The sync script treats managed #sids.in... tag lines as Bear metadata: it ignores them for change detection and strips them before writing Bear content back to local Markdown.
For readability in Bear, synced Bear notes also get a Markdown title copied from frontmatter immediately after the YAML block:
---
title: "Post Title"
...
---
# Post TitleThat Bear-only heading is ignored for change detection and stripped before writing content back to content/posts. If the Bear heading and frontmatter title differ, the frontmatter title wins the next time the note is synced from files to Bear.