claude-plugins / site
CLAUDE.md1 starsChanged 8 days agoMITView on GitHub
The maintainer's context for this tree. The repo-wide rules, the branch model and the release ritual are the root CLAUDE.md; the tag families and the workflows are .claude/docs/ci-and-releases.md. An Astro 6 static site (output: "static", every route prerendered into dist/), served by Cloudflare Workers Static Assets with no Worker script: wrangler.jsonc names the dist/ directory, a 404-page fallback and the custom-domain route, and wrangler deploy uploads the tree. It publishes the landing page and the user manual — the vwf…
In this repository7 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- installer/CLAUDE.mdCLAUDE.md
- plugin-authoringSkill
- releaseSkill
- skill-kitSkill
- stackgen-pluginSkill
- vwf-pluginSkill
The file
We split this CLAUDE.md into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 97
All under .config/mise/tasks/p/site/, all run from this directory:
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
site/ — the website at claude-plugins.virajp.dev
The maintainer's context for this tree. The repo-wide rules, the branch model
and the release ritual are the root CLAUDE.md; the tag
families and the workflows are
.claude/docs/ci-and-releases.md.
What it is
An Astro 6 static site (output: "static", every route prerendered into
dist/), served by Cloudflare Workers Static Assets with no Worker script:
wrangler.jsonc names the dist/ directory, a 404-page fallback and the
custom-domain route, and wrangler deploy uploads the tree. It publishes the
landing page and the user manual — the vwf and stackgen references, the how-to
guides and the installer reference — from the same markdown the repo ships, so
GitHub and the site read one authored tree. Every manual page is also served as
that authored markdown at /<id>.md, indexed by /llms.txt, so an agent never
has to parse the rendered HTML.
The tree
| Path | Is |
src/content/docs/ | the authored manual — plugins/, how-to/, installer/ — the one content collection, declared in src/content.config.ts |
src/pages/index.astro | the hand-authored landing page, copy verbatim from the design mockup, no client JS, one WebSite JSON-LD block |
src/pages/[...slug].astro | one route per manual page; src/pages/plugins/index.astro is the generated section index (the other two sections have their own index.md) |
src/pages/[...path].md.ts | the markdown mirror — one endpoint per collection entry, so a page at /<id>/ also exists at /<id>.md. An extension route never takes a trailing slash, whatever trailingSlash says. The generated /plugins/ index has no source entry and gets no mirror |
src/pages/llms.txt.ts, llms-full.txt.ts | the agent index and the whole manual in one file, both text/plain — llms.txt is the llmstxt.org shape (one ## per nav section, each entry linked at its .md URL), llms-full.txt every mirror concatenated in the same order |
src/lib/routes.ts | the route rule — the tree mirrored with no docs prefix, trailing slash, a section's index.md as the section's route. Both the pages and the link plugin call it, so they agree |
src/lib/remark-docs-links.ts | the link rule — rewrites every relative .md (or bare directory) link to its route at build time and fails the build on one that leaves the collection |
src/lib/markdown.ts | the mirror helper the three endpoints share — # <title>, a blank line, then the authored body, with every relative link made an absolute .md URL, because a fetched .md has no base to resolve against |
src/nav.ts | the sidebar model: three sections in a fixed order, entries sorted by frontmatter order |
src/layouts/, src/components/ | Base and Docs layouts; nav, Pagefind search (its Component UI modal), footer and the mark. Base owns the whole head — canonical, the icon and manifest links, the full OpenGraph and Twitter card set — and two optional props: markdown, the page's mirror emitted as a rel="alternate" link, and jsonLd. Docs passes both, its objects being a TechArticle and a BreadcrumbList mirroring the rendered crumb |
src/styles/ | tokens.css defines the design tokens, global.css consumes them |
src/scripts/mermaid.ts | client-side Mermaid, loaded only on a page whose markdown source contains a ```mermaid fence ([...slug].astro tests the entry body, not the rendered HTML) |
scripts/check-links.ts | the gate, in two passes. HTML: every internal href and src in dist/**/*.html resolves to a built file, every #fragment to an id in its target. Markdown: every absolute site URL in dist/**/*.md, llms.txt and llms-full.txt resolves the same way, and every docs page carries exactly one markdown alternate link, pointing at a file that exists |
public/brand/ | the brand set, served verbatim at /brand/ — the marks, vwf-favicon.svg (also the source p:site:icons rasterizes from) and social-preview.png (the readme's header image) |
public/ (root) | robots.txt, which carries the Content Signals line (search, ai-input, ai-train, all yes) as well as the sitemap pointer; the rasterized favicon set p:site:icons writes and this repo commits (favicon.ico, apple-touch-icon.png, icon-192.png, icon-512.png); site.webmanifest; and _headers, the Workers Static Assets headers file — HSTS, nosniff, frame denial, the referrer and permissions policies, the CSP, and the immutable cache rule for /_astro/*. Cloudflare parses _headers and never serves it. .config/linter.yaml ignores the whole tree |
wrangler.jsonc | the Workers Static Assets config; credentials never live here |
Rules
- Frontmatter is strict YAML —
title,description,order(integer), all required by the collection schema. A missing or mistyped key failsastro buildloudly withInvalidContentEntryDataError, naming the file and the offending field, so nothing reaches the link checker; a YAML syntax error fails the same way. Add the three keys before writing the body. - Relative
.mdlinks only inside the collection. Anything that must point outside it — the readme,installer/CLAUDE.md, the stackgen inventory — is an absolutehttps://github.com/virajp/claude-plugins/blob/main/<path>URL with its anchor kept. The remark plugin throws on a relative link that escapes, naming the file, line and href. - Never add a slug plugin. Astro 6's default heading ids keep trailing
hyphens and match GitHub's, so the manual's ~60 cross-links to command
headings (
#vwfarchitecture) resolve unchanged. A customrehype-slugorheadingIdCompatbreaks them. - Design rules, from the design system: dark only, radius 0 everywhere (the
app-icon tile is the one rounded shape), Geist Mono is semantic (the wordmark,
commands, paths, versions and table headers — never body text), and no
em-dashes anywhere on the site's own copy. The moved markdown keeps its
em-dashes; the rule binds
.astrofiles and the landing copy. - Nothing inline may reach the built HTML. The CSP in
public/_headersallows scripts from'self'only (plus'wasm-unsafe-eval', which is Pagefind's WASM), so no built page may carry an inline<script>. Authoring one in a.astrofile is fine: a plain<script>block is hoisted to a file by Astro —Docs.astro's copy-button block is exactly that — andassetsInlineLimit: 0is what stops Vite putting it back. What the CSP forbids is inline script content, not theis:inlinedirective: a<script>carrying a body must reach the browser as a file, sois:inlineon one of those is the mistake — it defeats the hoist and leaves the body in the HTML. Asrc=-only script carries no body and is safe either way;Search.astro's Pagefind loader is exactly that,src=plusis:inline. A script with enough substance to want its own module goes insrc/scripts/and is referenced bysrc, likemermaid.ts. Styles are'self' 'unsafe-inline'— the inline allowance exists for Shiki's per-tokenstyle=colours and mermaid's injected<style>elements, not as licence to hand-write one. The JSON-LD blocks areis:inlinedata, not executable script. The exact policy string lives in_headersand nowhere else. - Base's head is the only place for a meta tag. A page never writes one; it
passes
titleanddescription, and where it has them the two optional propsmarkdown(its mirror path) andjsonLd(one schema.org object or several). Anything a page needs in the head becomes aBaseprop. - Latin subsets only. The six fontsource imports in
Basearelatin-<weight>.css— Geist Mono 300/400/500, Schibsted Grotesk 400/500/700 — because the site is English-only and the full packages ship two dozen subset files nothing renders. Adding a weight means adding itslatin-<weight>.cssimport, never the bare<weight>.css. - Plain
<img>frompublic/brand/. Noastro:assets, no<Image>, so nosharpbuild —pnpm-workspace.yamldenies it explicitly. The one rasterizer isp:site:icons, which shells out to a one-offpnpx sharp-clifrom a temp directory and commits its PNGs; nothing rasterizes at build time and nothing enters the workspace. pnpx, nevernpx, in anything written here.
Tasks
All under .config/mise/tasks/p/site/, all run from this directory:
| Task | Does |
p:site:dev | astro dev |
p:site:build | astro build, then pagefind --site dist (the search index, written into dist/pagefind/) |
p:site:check | the gate: astro check, p:site:build, then scripts/check-links.ts over both the HTML and the markdown mirror. What site.yml and p:site:release run |
p:site:icons | rasterizes the favicon set — favicon.ico, apple-touch-icon.png, icon-192.png, icon-512.png — from public/brand/vwf-favicon.svg, through one-off pnpx runs of sharp-cli and png-to-ico in a temp directory. The outputs are committed, so this runs by hand when the mark changes, never in p:site:check and never in CI |
p:site:version | pnpm version <target> --no-git-tag-version in site/, skipping past a version with a 13 or 17 component — it computes the target first, stepping the bumped component past the number at the same level, then writes it in that one call and prints what it skipped, so 1.1.12 patched is 1.1.14; it refuses before writing anything when the level it was given can never reach the forbidden component. No commit, no tag; p:site:release calls it, and it stays for hand use |
p:site:release | from develop on a clean tree: bumps site/package.json from the last site-v* tag at the higher of the site level in .config/releases.yaml and the one an untagged version implies, clears the key, commits, pushes, merges to main with code:merge:main; there, refuses a version carrying a 13 or 17 component and an existing tag, runs p:site:check, cuts site-v<version>, pushes main then the tag, watches site.yml, and goes back to develop. --dry-run, --no-commit and --tag-only as the release skill says |
None of them runs in plugins.yml or in pre-commit. astro check is the site's
type gate; the site is not in vitest.config.mts or the root tsc lines,
and tsconfig.json extends astro/tsconfigs/strict rather than
tsconfig.base.json, whose composite + noEmit, types: ["node"] and
allowImportingTsExtensions conflict with Astro's.
The release model
The version lives in site/package.json (starts 1.0.0), and nobody bumps it
by hand. A landing plan records the site's level in .config/releases.yaml;
p:site:release (or p:release, for every project at once) runs from
develop, bumps from the last site-v* tag, commits, merges to main and tags
there. .github/workflows/site.yml has two jobs: build gates every PR and
push touching site/**, and deploy runs only on a pushed site-v* tag,
after verifying the tag matches the package version and is reachable from main
— the same two checks release.yml makes. A merge to main ships nothing
until a tag is cut. The deploy needs the repository secrets
CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID. Every site-v* tag carries
a GitHub Release in the release skill's note format — run /release, and ask
the user before p:site:release.
The design source
The design was authored in the Claude Design project
bb1f0a69-4c72-4f0e-91fd-186a963b568b ("claude-plugins (site)") as
Site Design System.dc.html, Site - Landing.dc.html and
Site - Docs.dc.html. The project archive, exported on 2026-10-28 before
standalone Claude Design closes on 2026-12-14, is committed under
docs/design/site/ byte-for-byte — one folder per design version
(option-a-drafting-table/, option-b-manual/, option-c-schematic/,
final/, logo/), each with its own assets/ and support.js so it renders
alone; dprint and the linter exclude the tree. The landing is the composite
chosen there, copy verbatim; the docs page is its option C. Tokens are in
src/styles/tokens.css.
Traps
- The 10-hour dependency cooldown.
pnpm-workspace.yaml'sminimumReleaseAgeblocks any package version published less than 600 minutes ago, so a freshly released Astro or Wrangler is not installable until the window passes. Check the age before bumping. - Astro 6 heading ids already match GitHub — see Rules. The temptation to add a slug plugin is the trap.
site/CLAUDE.mdis outside the collection (src/content/docs/), so it never becomes a page; keep it that way.- Dependency builds are denied, not allowed.
sharp(via astro) andworkerd(via wrangler) arefalseinallowBuilds; without those entries pnpm 11 fails the install withERR_PNPM_IGNORED_BUILDS. astro.config.ts'sinlineStylesheets: "never"andassetsInlineLimit: 0exist for the CSP alone. They are what stop Astro inlining a small stylesheet and Vite inlining the hoistedDocs.astroscript. Remove either and every local check still passes —astro check, the link checker, the W3C validator all read a page that looks fine — while the deployed page drops its styles or its sidebar, because Cloudflare applies a CSP the local build never sees._headersmust live inpublic/.site.yml'sdeployjob ships thebuildjob's artifact and never rebuilds, so anything that must reach Cloudflare has to be insidedist/at build time, andpublic/is the only route there.- Do not touch the
.pagefindcontent type. The two WASM binaries Pagefind's runtime streams —wasm.en.pagefindandwasm.unknown.pagefind— carry that extension; overriding the type in_headersor in wrangler breaks search with no build error. The index itself is under other extensions:index/*.pf_indexandfragment/*.pf_fragmentsit in subdirectories, while the singlepagefind.en_<hash>.pf_metasits indist/pagefind/beside the two binaries. - A
#fragmenton a.mdlink is skipped, not failed. The markdown mirror has no ids to resolve against, so the markdown pass resolves the file and leaves the fragment alone —check-links.tsgates that check on the target ending.html, andmarkdown.tsemits such links deliberately. Only the HTML pass, where every target is a page, fails a fragment that resolves to nothing. - This repo owns
robots.txtonly because Cloudflare is not managing it. The zone setting is Manage your robots.txt → Disable robots.txt configuration, and until 2026-09-05 it was on, so the served file was Cloudflare's Content Signals preamble plusDisallow: /for nine AI crawlers, with this file appended underneath. Re-enable that setting and Cloudflare's block comes back on top of aContent-Signalline that now says the opposite, so the file would contradict itself. Blocking is enforced separately again, by the AI bot policies (Search, Agent, Training), which are what actually returned 403 to ClaudeBot and GPTBot while the file already readAllow: /— robots.txt only asks. - The manifest is
site.webmanifest, notmanifest.json. The extension keeps dprint's json plugin off it. Renaming it puts the file back under a formatter that will reflow it. - The linter ignores the whole of
site/public/, not justbrand/— the.config/linter.yamlentry issite/public/**: the linter has no config for an ICO, a PNG or an extensionless_headers, and any path it has no config for produces warnings that fail the run. The ignore lives there and only there — thelintpre-commit hook callsmise run code:lint, which runs the linter over the whole tree, so there is no hook exclude and no pre-commit argument list to narrow it. **/*.astrois dprint-formatted through the markup plugin and ignored by the linter in.config/linter.yaml;site/**/*.csshas its own linter config block.src/content/docs/**/*.mdis dprint-formatted like the rest of the repo's markdown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

