Guidance for AI coding agents working on this repository.
This is the source of the RNP project website, https://www.rnpgp.org — a
static site built with Astro 7 (Node ≥ 22.12 required), styled with
Tailwind CSS 4 (via @tailwindcss/vite, Vite 8 / Rolldown) and made
interactive with Vue 3 islands (@astrojs/vue). RNP itself (the C++
OpenPGP/LibrePGP library) lives in a separate repository
(rnpgp/rnp); this repository contains only the
website.
- Content is AsciiDoc (blog posts, advisories — rendered at build time by Asciidoctor.js via a custom content loader) and Markdown (software, specs). Write new content in English.
- Design system: "The Cryptographer's Paper" — IBM Plex (Plex Sans for
text/display, Plex Mono for code/fingerprints/labels, self-hosted in
public/fonts/), light-first pastel look with full dark mode, brand colors blue#1A7BEC/ teal#00DFB7/ gold#FFDC4A/ navy#14172B, and the blue→teal brand gradient hero. - Deployed to GitHub Pages by GitHub Actions (see "CI" below).
astro.config.mjs— Astro config (site URL, vue, sitemap, Tailwind Vite plugin, Shiki dual themes).src/content.config.ts— content collections:blog,advisories(AsciiDoc via custom loader),software,specs(Markdown glob loaders),softwareDocs,manpages(AsciiDoc overvendor/). Also definesRNP_VERSIONfor man pages.src/lib/asciidoc.ts— Asciidoctor.js wrapper + the custom content-layer loader (adocLoader) that renders.adoc→ HTML and stores it asrendered.html, so entries work withrender(entry)/<Content />.src/lib/github.ts— build-time "latest release" fetch with pinned fallback.src/lib/tags.ts— namespaced tag labels (writtenin:C++→ "Written in C++").src/content/blog/*.adoc— blog posts, oneYYYY-MM-DD-slug.adocper post. URL:/blog/<id>/(id = filename). Front matter:title,date,categories,authors(name/email/social_links),excerpt, optionalredirect_from(legacy/blog/DD-MM-YYYY/.../URLs are emitted as redirect pages).src/content/advisories/*.adoc— security advisories. Front matter:title,date,id(e.g.RI-2021-01),cve_id,excerpt. Advisories are dual-published with open.ribose.com (check there for new ones). CVE JSON 5.0 records are self-hosted inpublic/cves/and linked from the advisory pages — when adding an advisory, add its JSON record too.src/content/software/*.md— product pages. Front matter:title,description,repo_url,tags,external_links({url, title}), optionaldocs_repo/docs_subtree/docs_ref(enables on-site docs), optionalfeature_with_priority(home-page featuring).src/content/specs/*.md— IETF draft specs (full text in a fenced block).src/pages/— routes, incl.rss.xml.ts,robots.txt.ts,404.astro.src/layouts/—BaseLayout.astro(shell: header, footer, SEO/OG, theme script, ClientRouter),DocsLayout.astro(product docs with sidebar).src/components/— Astro components;src/components/vue/— hydrated Vue islands (SiteHeader,SiteSearch,HeroDecrypt,FingerprintPlayground,InstallTabs,BlogBrowser,KeyFingerprintCards,ThemeToggle,Reveal,CopyButton).src/styles/global.css— Tailwind import,@themetokens, semantic light/dark CSS variables, prose/asciidoctor skin, Shiki dual-theme CSS.public/— fonts, brand SVGs,openpgp_keys/*.asc, favicons,og.png.scripts/fetch-sources.mjs— sparse-clones software repos intovendor/.vendor/— pulled-in repos (gitignored; delete to force a refresh).
fetch-sources.mjs (auto-run by predev/prebuild) sparse-clones each
software entry's docs_repo into vendor/<name>/ (blob-less, depth 1, pinned
to docs_ref when set) and checks out only README*, the docs_subtree
directory, and — for rnp — the man-page sources
(src/rnp/rnp.1.adoc, src/rnpkeys/rnpkeys.1.adoc, src/lib/librnp.3.adoc).
The softwareDocs and manpages collections render from vendor/; if it is
missing (e.g. offline), those collections are empty and the build continues
with a warning. Existing clones are reused — rm -rf vendor to refresh.
To update man pages / embedded docs after a new RNP release: bump
docs_ref in src/content/software/rnp.md, RNP_VERSION in
src/content.config.ts, the fallback in src/lib/github.ts, then
rm -rf vendor/rnp && npm run build.
npm install— install dependencies.npm run dev— dev server (runs fetch-sources first).npm run build— production build todist/(fetch-sources + Pagefind).npm run preview— serve the production build locally.npm run fetch-sources— refreshvendor/manually.npm run check:links— verify every internal href/src indist/exists.npm run check:links:external— full lychee check (internal + external) perlychee.toml(accept 429; excludes our own production URLs and bot-blocked sites). Must pass before merging; CI runs the same check.npm run test:e2e— interactive end-to-end checks (drives headless Chrome via CDP against a running preview on :4325; macOS Chrome required).
There is no unit test suite; validation is building + link checking.
- Styling: use the semantic Tailwind utilities (they auto-adapt to dark
mode):
bg-background,bg-surface,bg-surface-dim,text-foreground,text-muted,text-faint,border-line,text-accent,text-accent2. Do not writedark:variants for them. Custom classes from global.css:.mono-label,.card,.card-hover,.gradient-rule,.graph-bg,.btn,.btn-primary,.btn-ghost,.fingerprint. - Imports: use the
@/alias (@/components/...,@/layouts/...). - Islands: interactivity goes in Vue SFCs under
src/components/vue/, hydrated withclient:visible(below the fold) orclient:load; props must be JSON-serializable.prefers-reduced-motiongates passive animation only (auto-play, scroll-driven) — never user-triggered animation the visitor explicitly asked for (eggs, logo play, replays). - WAAPI easter eggs (keep them working, don't spoil them in visible copy):
header logo on the home page — hover wobble, click pulse, every 3rd click is
the "ribosome shuffle" (lenses split and snap back); typing
rnporpgpsummons the oracle rain — a matrix-style glyph fall in brand colors whose characters converge and hover into a random ancient quote (12 MOTD messages, east & west: Heraclitus, Lao Tzu, Pythagoras, Sun Tzu, Zhuangzi (AM I A BUTTERFLY DREAMING OF A MAN?), Confucius, Diamond Sutra, Plato, Socrates, Marcus Aurelius…) before dissolving with an attributed toast; typingdecryptreplays the hero scramble; the hero watermark rotates on scroll viaScrollTimeline. All are covered byscripts/e2e-cdp.mjs(28 checks). - Search: Pagefind index is generated post-build (
pagefind --site dist); theSiteSearchisland loads/pagefind/pagefind.jsat runtime. In dev,predevcopies the latest built index topublic/pagefind/(gitignored) viascripts/prepare-dev-search.mjs— runnpm run buildonce after checkout for search to work inastro dev. - Dead links: when an external link dies, point it at a Wayback Machine
snapshot (
https://archive.org/wayback/available?url=<url>×tamp=<yyyymmdd>finds the closest capture) instead of a substitute page — the snapshot preserves exactly what the content referenced. Upstream/vendor links are fixed viaUPSTREAM_LINK_FIXESinsrc/content.config.ts. - Astro 7 notes:
src/content.config.ts(notsrc/content/config.ts);zfromastro/zod;entry.id+render(entry)(noslug/entry.render()); Asciidoctor.js 4.x is async (await load(),await doc.convert()); whitespace between adjacent inline elements in.astrocollapses JSX-style — use{' '}.
.github/workflows/build_deploy.yml— Node 22,npm ci,fetch-sources, build, deploydist/to GitHub Pages (deploy only frommain)..github/workflows/links.yml— builds the site and runs lychee overdist/**/*.htmlpre-deploy (config:lychee.toml); fails on broken links..github/workflows/links_live.yml— post-deploy lychee crawl of the live site (lychee-live.toml, which does not exclude www.rnpgp.org) after each successful deploy + weekly; this is where the absolute canonical/OG URLs get verified in production..github/workflows/codeql.yml— CodeQL analysis.
Gemfile.lock,_site/,vendor/,dist/,node_modules/,parent-hub/are gitignored.package-lock.jsonis committed — keep it in sync.- Old blog URLs (
/blog/DD-MM-YYYY/slug/) are redirect pages generated from posts'redirect_fromfront matter — keep that field when touching old posts. - The
openpgp_keys/*.ascfiles are release-signing keys; treat them as immutable unless a key actually rotates. The keys page is/openpgp-keys/(hyphenated); the legacy/openpgp_keys/URL redirects there, while the.ascfiles keep the underscore prefix so published key-file URLs stay stable. - Man-page/description strings in
src/content.config.tsare parsed from the AsciiDoc== NAMEsections — if upstream renames sections, descriptions fall back to empty. parent-hub/was a legacy nested checkout from the Jekyll theme era; it has been removed — the footer links to open.ribose.com directly.- Tailwind v4 important syntax: the important modifier is a suffix
(
border-teal!), not the v3 prefix (!border-teal). You rarely need it — custom classes in@layer components(.mono-label,.btn, …) lose to plain utilities anyway. - Teleport + SSR: never render
<Teleport>unconditionally in an SSR-hydrated island — guard it (v-if="mounted", set inonMounted) or the hydration tree mismatches ("rendered on server: script, expected: Symbol(v-cmt)"). - Runtime-only URLs: dynamic
import('/pagefind/pagefind.js')breaks dev/SSR transforms even with@vite-ignore; usenew Function('u', 'return import(u)')so Vite never sees the specifier.