Skip to content
joelclaw-web logo

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 ...

SKILL.md

Full skill instructions

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):

RoleAliasSource
Mac Mini (control plane)OverlookThe Shining
NAS (archive)DerryIT
Laptop (dev machine)FlaggThe Dark Tower
Linux serverBlaineThe Dark Tower
Router (exit node)TodashThe 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

  1. Generate slug from title/​topic.
  2. Upsert contentResources with resourceId = article:<slug>, type = "article", full MDX body in fields.content, and fields.draft = true.
  3. Set fields.date to current ISO timestamp.
  4. Return slug + draft preview link (/<slug> if draft-visible in dev).

publish article <slug>

  1. Read article:<slug> from Convex.
  2. Patch/​upsert with fields.draft = false and fields.updated = now.
  3. 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
  4. 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

FilePurpose
app/​layout.tsxRoot layout, fonts, metadata, footer
app/​page.tsxHome page (post list)
app/[slug]/​page.tsxPost detail pages
app/​adrs/​page.tsxADR list
app/​adrs/[slug]/​page.tsxADR detail (strips H1 to avoid duplicate title)
app/​cool/​page.tsxCool/​discoveries list
app/​network/​page.tsxInfrastructure status page
components/​site-header.tsxHeader with active nav (client component)
components/​mobile-nav.tsxMobile overlay nav
components/​search-dialog.tsx⌘K search
lib/​posts.tsArticle loading from Convex (article:*)
lib/​adrs.tsADR loading from Convex (adr:*)
lib/​discoveries.tsDiscovery loading from Convex (discovery:*)
lib/​constants.tsSite name, URL, tagline
lib/​claw.tsSVG 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

  1. Draft in Convex (contentResources.upsert, resourceId = article:<slug>, draft: true).
  2. Use ISO datetime in date field.
  3. Add images to apps/​web/​public/​images/<slug>/ if needed and reference /​images/<slug>/​... in MDX.
  4. Publish by setting draft: false, then revalidate tags + paths (post:<slug>, article:<slug>, articles, /, /<slug>, /<slug>.md, /<slug>/​md, /​feed.xml, /​sitemap.md).
  5. 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:

  1. Check actual system state (kubectl get pods, tailscale status, launchctl print, etc.)
  2. Apply OPSEC rules — alias all hostnames, strip ports/​IPs/​usernames
  3. Keep data arrays at top of file for easy updates
  4. Status dots: green (Online) with ping animation, yellow (Idle), gray (Offline)