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
Public diagrams use generic role names such as control plane, archive, development client, relay, and router. Do not publish real hostnames disguised as fictional aliases; the private topology boundary still applies.
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:*)
apps/web/lib/convex-api.ts → checked web-facing Convex function contract plus the runtime anyApi proxy
- Never add a
link: dependency on the sibling private joelclaw-api checkout. Vercel clones only this repository.
- 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
contentResources with resourceId = article:<slug>, type = "article", full MDX body in fields.content, and fields.draft = true.
- Set
fields.date to 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 = false and fields.updated = now.
- Revalidate all affected surfaces via
POST /api/revalidate with:
- tags:
post:<slug>, article:<slug>, articles
- paths:
/, /<slug>, /<slug>.md, /<slug>/md, /feed.xml, /sitemap.md
- Verify
/, /<slug>, /<slug>.md, and /feed.xml include 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
MobileNav component
- 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/convex-api.ts |
Deployable runtime proxy and typed public Convex contract |
lib/constants.ts |
Site name, URL, tagline |
lib/claw.ts |
SVG path for claw icon |
ADR Display Rules
- ADR runtime source is Convex (
contentResources with resourceId = 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
date field.
- 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.ts for article resources
scripts/seed-adrs-discoveries.ts for 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)
1---2name: joelclaw-web3description: Update and maintain joelclaw.com — the Next.js web app at apps/web/. Use when writing blog posts, editing pages, updating the network page, changing layout/header/footer, adding components, or fixing anything on the site. Hard content triggers: 'write article about X' (draft in Convex), 'publish article <slug>' (set draft=false + revalidate tags/paths). Also triggers on: 'update the site', 'write a post', 'fix the blog', 'joelclaw.com', 'update network page', 'add a page', 'change the header', or any task involving the public-facing web app.4---5
6# joelclaw.com Web App
7
8Next.js app at `apps/web/` in the joelclaw monorepo (`~/Code/joelhooks/joelclaw/`).
9Deployed to Vercel on push to `main`. Dark theme, minimal, system-y aesthetic.
10
11## OPSEC Rules
12
13**Never expose real infrastructure details on public pages.**
14
15- **Node/host names**: Use Stephen King universe aliases, never real tailnet hostnames
16- **Ports**: Do not publish port numbers for any service
17- **Usernames**: Strip `com.joel.` or similar prefixes from service identifiers
18- **IPs/subnets**: Never show real IP addresses or CIDR ranges
19- **Brands/models of network gear**: Generalize (e.g. "NAS" not "Synology DS1821+")
20- **Tailscale**: OK to mention as the mesh VPN product, but no tailnet name or node IPs
21
22Public diagrams use generic role names such as control plane, archive, development client, relay, and router. Do not publish real hostnames disguised as fictional aliases; the private topology boundary still applies.
23
24## Content Model (Convex-first)
25
26Canonical runtime content lives in Convex `contentResources`:
27- `article:<slug>` (`type = article`)
28- `adr:<slug>` (`type = adr`)
29- `discovery:<slug>` (`type = discovery`)
30
31Filesystem content under `apps/web/content/` is seed/backfill material, not runtime source.
32
33Runtime read policy:
34- `apps/web/lib/posts.ts` → Convex-first articles (`article:*`)
35- `apps/web/lib/adrs.ts` → Convex-first ADRs (`adr:*`)
36- `apps/web/lib/discoveries.ts` → Convex-first discoveries (`discovery:*`)
37- `apps/web/lib/convex-api.ts` → checked web-facing Convex function contract plus the runtime `anyApi` proxy
38- Never add a `link:` dependency on the sibling private `joelclaw-api` checkout. Vercel clones only this repository.
39- Optional local escape hatches (non-production only):
40 - `JOELCLAW_ALLOW_FILESYSTEM_POSTS_FALLBACK=1` (articles)
41 - `JOELCLAW_ALLOW_FILESYSTEM_CONTENT_FALLBACK=1` (ADRs/discoveries)
42
43Article fields still mirror MDX frontmatter shape:
44
45```yaml
46---
47title: "Post Title"
48type: "article" | "essay" | "note" | "tutorial"
49date: "2026-02-19T11:00:00" # ISO datetime, NOT just date
50updated: "2026-02-19T14:30:00" # optional, bumps sort position
51description: "One-liner for cards and meta"
52tags: ["tag1", "tag2"]
53draft: true # optional, hides from prod
54source: "https://..." # optional, for video-notes
55channel: "Channel Name" # optional, for video-notes
56duration: "00:42:02" # optional, for video-notes
57---
58```
59
60**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.
61
62**Slugs**: Derived from `fields.slug` in Convex (`resourceId = article:<slug>`).
63
64## Hard Trigger Workflow
65
66### `write article about X`
67
681. Generate slug from title/topic.
692. Upsert `contentResources` with `resourceId = article:<slug>`, `type = "article"`, full MDX body in `fields.content`, and `fields.draft = true`.
703. Set `fields.date` to current ISO timestamp.
714. Return slug + draft preview link (`/<slug>` if draft-visible in dev).
72
73### `publish article <slug>`
74
751. Read `article:<slug>` from Convex.
762. Patch/upsert with `fields.draft = false` and `fields.updated = now`.
773. Revalidate all affected surfaces via `POST /api/revalidate` with:
78 - tags: `post:<slug>`, `article:<slug>`, `articles`
79 - paths: `/`, `/<slug>`, `/<slug>.md`, `/<slug>/md`, `/feed.xml`, `/sitemap.md`
804. Verify `/`, `/<slug>`, `/<slug>.md`, and `/feed.xml` include the published post.
81
82## Media Embeds
83
84**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.
85
86## Writing Voice
87
88Use 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.
89
90## Design System
91
92- **Theme**: Dark (`bg-[#0a0a0a]`), neutral grays, `--color-claw: #ff1493` (hot pink accent)
93- **Fonts**: Geist Sans (body), Geist Mono (code/data), Dank Mono (code blocks with ligatures)
94- **Content width**: `max-w-2xl` (672px) — intentionally narrow for reading
95- **Header**: Single row — claw icon + "JoelClaw" left, nav links + search right. No tagline in header.
96- **Nav items**: Writing (`/`), Cool (`/cool`), ADRs (`/adrs`), Network (`/network`)
97- **Active nav**: White text vs neutral-500 for inactive, detected via `usePathname()`
98- **Search**: ⌘K dialog using pagefind, type-based icons/badges
99- **Mobile**: Full-screen overlay nav via `MobileNav` component
100- **Code blocks**: Catppuccin Macchiato theme, rehype-pretty-code
101- **Sidenotes**: Tufte-style CSS sidenotes (pure CSS, no JS)
102
103## Key Files
104
105| File | Purpose |
106|------|---------|
107| `app/layout.tsx` | Root layout, fonts, metadata, footer |
108| `app/page.tsx` | Home page (post list) |
109| `app/[slug]/page.tsx` | Post detail pages |
110| `app/adrs/page.tsx` | ADR list |
111| `app/adrs/[slug]/page.tsx` | ADR detail (strips H1 to avoid duplicate title) |
112| `app/cool/page.tsx` | Cool/discoveries list |
113| `app/network/page.tsx` | Infrastructure status page |
114| `components/site-header.tsx` | Header with active nav (client component) |
115| `components/mobile-nav.tsx` | Mobile overlay nav |
116| `components/search-dialog.tsx` | ⌘K search |
117| `lib/posts.ts` | Article loading from Convex (`article:*`) |
118| `lib/adrs.ts` | ADR loading from Convex (`adr:*`) |
119| `lib/discoveries.ts` | Discovery loading from Convex (`discovery:*`) |
120| `lib/convex-api.ts` | Deployable runtime proxy and typed public Convex contract |
121| `lib/constants.ts` | Site name, URL, tagline |
122| `lib/claw.ts` | SVG path for claw icon |
123
124## ADR Display Rules
125
126- ADR runtime source is Convex (`contentResources` with `resourceId = adr:<slug>`)
127- Vault sync still updates repo snapshots under `apps/web/content/adrs/`
128- Project snapshots into Convex with: `bun scripts/seed-adrs-discoveries.ts`
129- 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
130- Regex: `content.replace(/^#\s+(?:ADR-\d+:\s*)?.*$/m, "").trim()`
131
132## Adding a New Post
133
1341. Draft in Convex (`contentResources.upsert`, `resourceId = article:<slug>`, `draft: true`).
1352. Use ISO datetime in `date` field.
1363. Add images to `apps/web/public/images/<slug>/` if needed and reference `/images/<slug>/...` in MDX.
1374. Publish by setting `draft: false`, then revalidate tags + paths (`post:<slug>`, `article:<slug>`, `articles`, `/`, `/<slug>`, `/<slug>.md`, `/<slug>/md`, `/feed.xml`, `/sitemap.md`).
1385. Verify route + markdown twin + homepage + feed consistency.
139
140Backfill scripts:
141- `scripts/seed-articles.ts` for article resources
142- `scripts/seed-adrs-discoveries.ts` for ADR + discovery resources
143
144## Network Page
145
146The network page (`app/network/page.tsx`) shows real infrastructure with aliased names. When updating:
147
1481. Check actual system state (`kubectl get pods`, `tailscale status`, `launchctl print`, etc.)
1492. Apply OPSEC rules — alias all hostnames, strip ports/IPs/usernames
1503. Keep data arrays at top of file for easy updates
1514. Status dots: green (Online) with ping animation, yellow (Idle), gray (Offline)