Joelclaw joelclaw-web
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.
git clone https://github.com/joelhooks/joelclaw
T=$(mktemp -d) && git clone --depth=1 https://github.com/joelhooks/joelclaw "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/joelclaw-web" ~/.claude/skills/joelhooks-joelclaw-joelclaw-web && rm -rf "$T"
skills/joelclaw-web/SKILL.mdjoelclaw.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
or similar prefixes from service identifierscom.joel. - 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:
→ Convex-first articles (apps/web/lib/posts.ts
)article:*
→ Convex-first ADRs (apps/web/lib/adrs.ts
)adr:*
→ Convex-first discoveries (apps/web/lib/discoveries.ts
)discovery:*- Optional local escape hatches (non-production only):
(articles)JOELCLAW_ALLOW_FILESYSTEM_POSTS_FALLBACK=1
(ADRs/discoveries)JOELCLAW_ALLOW_FILESYSTEM_CONTENT_FALLBACK=1
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
write article about X- Generate slug from title/topic.
- Upsert
withcontentResources
,resourceId = article:<slug>
, full MDX body intype = "article"
, andfields.content
.fields.draft = true - Set
to current ISO timestamp.fields.date - Return slug + draft preview link (
if draft-visible in dev)./<slug>
publish article <slug>
publish article <slug>- Read
from Convex.article:<slug> - Patch/upsert with
andfields.draft = false
.fields.updated = now - Revalidate all affected surfaces via
with:POST /api/revalidate- tags:
,post:<slug>
,article:<slug>articles - paths:
,/
,/<slug>
,/<slug>.md
,/<slug>/md
,/feed.xml/sitemap.md
- tags:
- Verify
,/
,/<slug>
, and/<slug>.md
include the published post./feed.xml
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 (
), neutral grays,bg-[#0a0a0a]
(hot pink accent)--color-claw: #ff1493 - Fonts: Geist Sans (body), Geist Mono (code/data), Dank Mono (code blocks with ligatures)
- Content width:
(672px) — intentionally narrow for readingmax-w-2xl - Header: Single row — claw icon + "JoelClaw" left, nav links + search right. No tagline in header.
- Nav items: Writing (
), Cool (/
), ADRs (/cool
), Network (/adrs
)/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
componentMobileNav - Code blocks: Catppuccin Macchiato theme, rehype-pretty-code
- Sidenotes: Tufte-style CSS sidenotes (pure CSS, no JS)
Key Files
| File | Purpose |
|---|---|
| Root layout, fonts, metadata, footer |
| Home page (post list) |
| Post detail pages |
| ADR list |
| ADR detail (strips H1 to avoid duplicate title) |
| Cool/discoveries list |
| Infrastructure status page |
| Header with active nav (client component) |
| Mobile overlay nav |
| ⌘K search |
| Article loading from Convex () |
| ADR loading from Convex () |
| Discovery loading from Convex () |
| Site name, URL, tagline |
| SVG path for claw icon |
ADR Display Rules
- ADR runtime source is Convex (
withcontentResources
)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 (
) strips the H1 from markdown content because the page already renders the title with ADR number prefixapp/adrs/[slug]/page.tsx - 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
field.date - Add images to
if needed and referenceapps/web/public/images/<slug>/
in MDX./images/<slug>/... - Publish by setting
, then revalidate tags + paths (draft: false
,post:<slug>
,article:<slug>
,articles
,/
,/<slug>
,/<slug>.md
,/<slug>/md
,/feed.xml
)./sitemap.md - Verify route + markdown twin + homepage + feed consistency.
Backfill scripts:
for article resourcesscripts/seed-articles.ts
for ADR + discovery resourcesscripts/seed-adrs-discoveries.ts
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
, etc.)launchctl print - 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)