From sitewarming-template-tools
Build and deliver a custom org-private astro template for a SiteWarming customer from their design.md (design spec). Use this whenever someone on the team needs to fulfil an approved custom-template request, turn a customer's design.md / design spec into a real template, "make a template for org X", generate the Layout/BlogList/BlogPost astro files for a customer, or register-and-deliver an org-private template. Trigger even when the person doesn't say "skill" — phrases like "here's a design.md, build the template", "the customer wants their own template", or "deliver template to this org" should all use this. This is the single supported path for custom templates; do NOT hand-edit the registry or POST to the admin API without it.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sitewarming-template-tools:build-custom-templateThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A SiteWarming customer (enterprise plan) submits a `design.md` describing the
A SiteWarming customer (enterprise plan) submits a design.md describing the
template they want. Any teammate — technical or not — can run this skill to turn
that design.md into a real org-private astro template and deliver it to the
customer's organization, without touching the worker, the DB, or any local MCP
server.
You (Claude) do the one part that needs judgment: authoring the three .astro
files from the design spec. Everything else is mechanical — file writes, a build
check, and two HTTP calls to the worker admin API. The worker is the source of
truth for the DB; this skill never writes to the DB directly.
Read the settings file .claude/sitewarming-template-tools.local.md (relative
to wherever the teammate is running Claude Code; if it's not there, look in the
astro repo root). It carries the per-machine config so nobody re-types it:
astro_repo_path — absolute path to the astro-warming-template checkoutworker_api_url — e.g. https://api.sitewarming.com or http://localhost:8787template_mcp_api_key — the secret that matches the worker's
TEMPLATE_MCP_API_KEY. Sent as the X-Template-MCP-Key header. Treat this
as a secret — never echo it back in chat or write it into any file you create.If that file is missing, see references/settings-template.md and ask the
teammate for the three values, then offer to write the settings file for them so
they never have to do it again.
From the teammate / request, you also need four things. Ask for any that are missing — keep it conversational, they may not know the jargon:
delivered, so the request must already be approved.acme-tours-1. Lowercase letters,
digits, dashes, underscores; max 100 chars; default is reserved. If they
don't offer one, propose a sensible slug from the org/brand name and confirm.You'll also pick a human name and one-line description for the template (for the portal picker), and a thumbnail_url. If there's no real thumbnail yet, use a placeholder URL and tell the teammate to replace it later — the API requires a non-empty value.
Work through these in order. The build check is a real gate: do not deliver a template that doesn't compile, because a broken template breaks the customer's live site when the astro repo deploys.
Your primary structural reference is an existing org-private template, not the
default. Read all three files in <astro_repo_path>/src/templates/vatsalorg-1/
(Layout.astro, BlogList.astro, BlogPost.astro). It's a clean, self-contained
template that consumes the PageBundle, wires the Connection/EmailCapture/Purchase
modals, and uses the 4-prop BlogList/BlogPost signatures — it's exactly the
shape you're producing. (The default/ template works too, but its Layout.astro
is ~2,200 lines of production-evolved logic coupled to many shared components —
read it only to confirm a PageBundle field exists, not as an authoring model.)
Skim <astro_repo_path>/src/lib/page-bundle.ts for the exact PageBundle field
names. Authoring against PageBundle is what guarantees the customer's data still
shows up. Every section the customer expects — FAQ, blog posts, sale mode, logo,
social, theme colors — flows through that bundle. If your template silently drops
a section, the customer loses content they had. Honor the same data the reference
template does, even when you restyle it.
A note on colors: the reference treats live themeTokens (the customer's saved
theme) as the source of the primary color, with the brand palette as a fallback —
unless the design.md explicitly dictates a fixed brand palette, in which case lock
the colors as constants (that's a deliberate brand-identity template). Use judgment
from the design spec.
Translate the design.md into:
Layout.astro — the full page. This is the main work. Match the design's
layout, typography, color palette, and section order, while reading data from
the PageBundle props (the page shim spreads the bundle in as props).BlogList.astro and BlogPost.astro — the blog index and detail rendering.
Even if your Layout inlines a few recent posts (the default does), these two
files must exist and compile, because the blog routes render through them.Match the surrounding code: these are .astro files with a frontmatter fence
(---) for the script and JSX-like markup below. Reuse the default template's
import style and prop destructuring so the build stays happy. Don't invent new
PageBundle fields — use the ones the default template uses.
Create the directory <astro_repo_path>/src/templates/<slug>/ and write your
three authored files there: Layout.astro, BlogList.astro, BlogPost.astro.
Then write the page shim at <astro_repo_path>/src/pages/<slug>.astro with
exactly this content (it's a fixed 4-line pattern — the worker enforces who's
allowed to see it; astro just renders):
---
// Custom template page shim — <slug> (org_private).
// Visibility / ownership is enforced by the worker; astro just renders.
import { fetchPageBundle } from '../lib/page-bundle';
import Layout from '../templates/<slug>/Layout.astro';
const bundle = await fetchPageBundle(Astro);
---
<Layout {...bundle} />
This is the step the registry line alone does not cover, and skipping it ships
a template whose homepage works but whose /blog and blog-detail pages silently
fall back to the default look. That's a real defect the customer would see, so
treat it as required.
The blog index and blog-detail pages dispatch to a template's BlogList /
BlogPost by an explicit per-slug conditional — they are not auto-resolved
from the registry. Edit both:
<astro_repo_path>/src/pages/blog.astro:
...BlogList imports:
import <Pascal>BlogList from "../templates/<slug>/BlogList.astro";{templateVersion === 'vatsalorg-1' && ( ... )} blocks:
{templateVersion === '<slug>' && (
<<Pascal>BlogList
templateVersion={templateVersion}
...
/>
)}
Copy the prop list from the vatsalorg-1 block right above — pass the exact
same props so it compiles and behaves.<astro_repo_path>/src/pages/blog/[slug].astro: do the same for BlogPost —
add import <Pascal>BlogPost from "../../templates/<slug>/BlogPost.astro"; (note
the ../../ — this file is one level deeper) and the matching
{templateVersion === '<slug>' && ( <<Pascal>BlogPost ... /> )} block, copying
props from the vatsalorg-1 block.
<Pascal> is the slug in PascalCase (e.g. acme-tours-1 → AcmeTours1). Use the
sibling vatsalorg-1 blocks as your exact template; matching them is what keeps
the build green and the data flowing.
Edit <astro_repo_path>/src/lib/template-registry.ts. Inside the
TEMPLATE_ROUTES map, add one line so the slug routes to its page:
'<slug>': '/<slug>',
Put it under the // ─── Org-private templates ── comment if that section
exists; otherwise add it just before the map's closing };. This is idempotent
— if a '<slug>': entry already exists, leave it alone.
Run the astro build to confirm the template compiles:
cd <astro_repo_path> && npm run build:original
Use build:original (plain astro build), not build — build runs extra
route-fixing steps you don't need for a compile check.
If it fails, read the error, fix the offending .astro file(s), and re-run.
Do not move on until it passes. A failing build means the template would break
the site on deploy.
Two HTTP calls to the worker admin API, both with the
X-Template-MCP-Key: <template_mcp_api_key> header. This is what makes the
template real for the customer: it creates the DB registry row (org-private,
owned by the customer org) and flips the request to delivered.
First, create the template:
curl -sS -X POST "<worker_api_url>/api/admin/astro-templates" \
-H "Content-Type: application/json" \
-H "X-Template-MCP-Key: <template_mcp_api_key>" \
-d '{
"id": "<slug>",
"name": "<human name>",
"description": "<one-line description>",
"thumbnail_url": "<thumbnail or placeholder url>",
"astro_entry_path": "/<slug>",
"visibility": "org_private",
"owner_org_id": "<organization_id>",
"status": "beta"
}'
If this returns a 409 because the template id already exists, check whether it's already this org's org-private template (a prior partial run). If it is, that's fine — continue to mark-delivered. If it belongs to a different org or is public, stop and pick a different slug.
Then mark the request delivered:
curl -sS -X POST "<worker_api_url>/api/admin/custom-template-requests/<request_id>/mark-delivered" \
-H "Content-Type: application/json" \
-H "X-Template-MCP-Key: <template_mcp_api_key>" \
-d '{ "assigned_template_id": "<slug>" }'
The request must be in approved state for this to succeed (you'll get a 409
invalid_state otherwise). The worker binds the template to the org and records
the delivery under a synthetic admin identity — you don't pass any user id, and
you shouldn't: the worker deliberately attributes this to a null admin user to
satisfy a foreign-key constraint. Don't try to add a granted_by / user field.
The template exists in the DB and the request is delivered, but the customer won't see it until the astro repo is committed and deployed. End by telling the teammate plainly:
Template
<slug>is built and delivered to org<organization_id>. To make it live, commit the astro changes (src/templates/<slug>/,src/pages/<slug>.astro,src/lib/template-registry.ts, and the blog-route edits insrc/pages/blog.astro
src/pages/blog/[slug].astro) and deployastro-warming-template.
Offer to make the commit if they want, but don't deploy on your own.
PageBundle field that
doesn't exist. Compare against the default template.template_mcp_api_key doesn't match the
worker's TEMPLATE_MCP_API_KEY, or you're pointing at the wrong
worker_api_url. Re-check the settings file.approved (maybe already
delivered, or still pending). Confirm the request_id and its status with the
teammate.This skill builds and delivers one org-private template per run. It does not deploy the astro repo, generate thumbnails, or handle the "guided" (non-design.md) request mode — those requests come in as structured fields, not a design spec, and aren't supported here.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub sitewarming/sitewarming-template-skill --plugin sitewarming-template-tools