joelclaw-web
joelclaw.com Web App
Next.js app at apps/web/ in the joelclaw monorepo (~/Code/joelhooks/joelclaw/).
Deployed to Vercel on push to main. Dark theme, minimal, system-y aesthetic.
OPSEC Rules
Never expose real infrastructure details on public pages.
- Node/host names: Use Stephen King universe aliases, never real tailnet hostnames
- Ports: Do not publish port numbers for any service
- Usernames: Strip
com.joel.or similar prefixes from service identifiers - IPs/subnets: Never show real IP addresses or CIDR ranges
- Brands/models of network gear: Generalize (e.g. "NAS" not "Synology DS1821+")
- Tailscale: OK to mention as the mesh VPN product, but no tailnet name or node IPs
Current King universe mapping (network page):
| Role | Alias | Source |
|---|---|---|
| Mac Mini (control plane) | Overlook | The Shining |
| NAS (archive) | Derry | IT |
| Laptop (dev machine) | Flagg | The Dark Tower |
| Linux server | Blaine | The Dark Tower |
| Router (exit node) | Todash | The Dark Tower |
Content Model (Convex-first)
Canonical runtime content lives in Convex contentResources:
article:<slug>(type = article)adr:<slug>(type = adr)discovery:<slug>(type = discovery)
Filesystem content under apps/web/content/ is seed/backfill material, not runtime source.
Runtime read policy:
apps/web/lib/posts.ts→ Convex-first articles (article:*)apps/web/lib/adrs.ts→ Convex-first ADRs (adr:*)apps/web/lib/discoveries.ts→ Convex-first discoveries (discovery:*)- Optional local escape hatches (non-production only):
JOELCLAW_ALLOW_FILESYSTEM_POSTS_FALLBACK=1(articles)JOELCLAW_ALLOW_FILESYSTEM_CONTENT_FALLBACK=1(ADRs/discoveries)
Article fields still mirror MDX frontmatter shape:
---
title: "Post Title"
type: "article" | "essay" | "note" | "tutorial"
date: "2026-02-19T11:00:00" # ISO datetime, NOT just date
updated: "2026-02-19T14:30:00" # optional, bumps sort position
description: "One-liner for cards and meta"
tags: ["tag1", "tag2"]
draft: true # optional, hides from prod
source: "https://..." # optional, for video-notes
channel: "Channel Name" # optional, for video-notes
duration: "00:42:02" # optional, for video-notes
---
Sorting: Posts sort by updated ?? date descending. Use full ISO datetimes (not bare dates) for deterministic ordering. Setting updated bumps a post to the top without changing its original publish date.
Slugs: Derived from fields.slug in Convex (resourceId = article:<slug>).
Hard Trigger Workflow
write article about X
- Generate slug from title/topic.
- Upsert
contentResourceswithresourceId = article:<slug>,type = "article", full MDX body infields.content, andfields.draft = true. - Set
fields.dateto current ISO timestamp. - Return slug + draft preview link (
/<slug>if draft-visible in dev).
publish article <slug>
- Read
article:<slug>from Convex. - Patch/upsert with
fields.draft = falseandfields.updated = now. - Revalidate all affected surfaces via
POST /api/revalidatewith:- tags:
post:<slug>,article:<slug>,articles - paths:
/,/<slug>,/<slug>.md,/<slug>/md,/feed.xml,/sitemap.md
- tags:
- Verify
/,/<slug>,/<slug>.md, and/feed.xmlinclude the published post.
Media Embeds
Always embed YouTube videos in /cool discoveries and articles when they add context. Use the <YouTube id="VIDEO_ID" /> MDX component (available in both .mdx articles and .md discoveries). Extract the video ID from the URL (youtube.com/watch?v=VIDEO_ID). Place embeds near the top of the relevant section, before the prose discussion.
Writing Voice
Use the canonical joel-writing-style skill for prose. Key traits: direct, first-person when the claim is actually Joel's, strategic profanity, short paragraphs, bold emphasis, conversational but technical. Never corporate-speak and never fabricate Joel's beliefs or philosophy.
Design System
- Theme: Dark (
bg-[#0a0a0a]), neutral grays,--color-claw: #ff1493(hot pink accent) - Fonts: Geist Sans (body), Geist Mono (code/data), Dank Mono (code blocks with ligatures)
- Content width:
max-w-2xl(672px) — intentionally narrow for reading - Header: Single row — claw icon + "JoelClaw" left, nav links + search right. No tagline in header.
- Nav items: Writing (
/), Cool (/cool), ADRs (/adrs), Network (/network) - Active nav: White text vs neutral-500 for inactive, detected via
usePathname() - Search: ⌘K dialog using pagefind, type-based icons/badges
- Mobile: Full-screen overlay nav via
MobileNavcomponent - Code blocks: Catppuccin Macchiato theme, rehype-pretty-code
- Sidenotes: Tufte-style CSS sidenotes (pure CSS, no JS)
Key Files
| File | Purpose |
|---|---|
app/layout.tsx |
Root layout, fonts, metadata, footer |
app/page.tsx |
Home page (post list) |
app/[slug]/page.tsx |
Post detail pages |
app/adrs/page.tsx |
ADR list |
app/adrs/[slug]/page.tsx |
ADR detail (strips H1 to avoid duplicate title) |
app/cool/page.tsx |
Cool/discoveries list |
app/network/page.tsx |
Infrastructure status page |
components/site-header.tsx |
Header with active nav (client component) |
components/mobile-nav.tsx |
Mobile overlay nav |
components/search-dialog.tsx |
⌘K search |
lib/posts.ts |
Article loading from Convex (article:*) |
lib/adrs.ts |
ADR loading from Convex (adr:*) |
lib/discoveries.ts |
Discovery loading from Convex (discovery:*) |
lib/constants.ts |
Site name, URL, tagline |
lib/claw.ts |
SVG path for claw icon |
ADR Display Rules
- ADR runtime source is Convex (
contentResourceswithresourceId = adr:<slug>) - Vault sync still updates repo snapshots under
apps/web/content/adrs/ - Project snapshots into Convex with:
bun scripts/seed-adrs-discoveries.ts - The detail page (
app/adrs/[slug]/page.tsx) strips the H1 from markdown content because the page already renders the title with ADR number prefix - Regex:
content.replace(/^#\s+(?:ADR-\d+:\s*)?.*$/m, "").trim()
Adding a New Post
- Draft in Convex (
contentResources.upsert,resourceId = article:<slug>,draft: true). - Use ISO datetime in
datefield. - Add images to
apps/web/public/images/<slug>/if needed and reference/images/<slug>/...in MDX. - Publish by setting
draft: false, then revalidate tags + paths (post:<slug>,article:<slug>,articles,/,/<slug>,/<slug>.md,/<slug>/md,/feed.xml,/sitemap.md). - Verify route + markdown twin + homepage + feed consistency.
Backfill scripts:
scripts/seed-articles.tsfor article resourcesscripts/seed-adrs-discoveries.tsfor ADR + discovery resources
Network Page
The network page (app/network/page.tsx) shows real infrastructure with aliased names. When updating:
- Check actual system state (
kubectl get pods,tailscale status,launchctl print, etc.) - Apply OPSEC rules — alias all hostnames, strip ports/IPs/usernames
- Keep data arrays at top of file for easy updates
- Status dots: green (Online) with ping animation, yellow (Idle), gray (Offline)