
Joelclaw Web
- 33 installs
- 61 repo stars
- Updated August 4, 2026
- joelhooks/joelclaw
Update and maintain joelclaw.com, the Next.js app at apps/web, including pages, layout, components, and Convex-backed blog content.
About
Covers editing the public joelclaw.com Next.js app and its Convex-first content model, with OPSEC rules for hiding real infra. A developer uses it to write posts, edit pages, or fix the site.
- OPSEC: never expose real hostnames, ports, IPs; use King-universe aliases
- Convex contentResources is runtime source; filesystem content is seed material
Joelclaw Web by the numbers
- 33 all-time installs (skills.sh)
- Ranked #1,438 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joelhooks/joelclaw --skill joelclaw-webAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 61 |
| Last updated | August 4, 2026 |
| Repository | joelhooks/joelclaw ↗ |
What it does
Update and maintain joelclaw.com, the Next.js app at apps/web, including pages, layout, components, and Convex-backed blog content.
Files
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
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
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
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.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:
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)
interface:
icon_small: "./assets/small-logo.svg"
icon_large: "./assets/large-logo.png"
<svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="JoelClaw icon">
<defs>
<clipPath id="circle-clip">
<circle cx="256" cy="256" r="248" />
</clipPath>
</defs>
<circle cx="256" cy="256" r="248" fill="#0a0a0a" />
<g clip-path="url(#circle-clip)">
<path fill="#ff1493" d="M175.656 22.375l-48.47 82.094c-23.017 4.384-43.547 11.782-60.124 22.374-24.436 15.613-40.572 37.414-45.5 67.875-4.79 29.62 1.568 68.087 24.125 116.093 93.162 22.88 184.08-10.908 257.25-18.813 37.138-4.012 71.196-.898 96.344 22.97 22.33 21.19 36.21 56.808 41.908 113.436 29.246-35.682 44.538-69.065 49.343-99.594 5.543-35.207-2.526-66.97-20.31-95.593-8.52-13.708-19.368-26.618-32-38.626l14.217-33-41.218 10.625c-8.637-6.278-17.765-12.217-27.314-17.782l-7.03-59.782-38.157 37.406c-12.418-5.186-25.184-9.804-38.158-13.812l-8.375-71.28-57.625 56.5c-9.344-1.316-18.625-2.333-27.812-2.97l-31.094-78.125zM222 325.345c-39.146 7.525-82.183 14.312-127.156 11.686 47.403 113.454 207.056 224.082 260.125 87-101.18 33.84-95.303-49.595-132.97-98.686z" />
</g>
</svg>