Source for hyperverse.world, the documentation and showcase site for the hyperverse R ecosystem.
Two tools, deliberately:
- Astro builds the site. Every page but the home page is static HTML plus a few inline scripts (theme toggle, hero canvas, copy buttons) and ships no framework runtime at all. The home page hydrates one component, the design system's live htmxr terminal, on Preact: about 19 KB gzipped.
- Quarto builds the talk decks, and only those.
revealjs (speaker notes, fragments,
r-fit-text, self-contained output) has no equivalent worth having on the Astro side.
Deployed to GitHub Pages on every push to main.
| Node | 22 or newer |
| Quarto | 1.9.37 (pinned in CI, see .github/workflows/publish.yml) |
R is not required. _data/*.yml is read directly by the Astro content
loader, and no deck contains an R chunk.
npm install
npm run talks # render the decks into public/talks/ (needed once)
npm run dev # http://localhost:4321, with hot reloadnpm run talks is only needed the first time, and afterwards whenever you edit
a deck. Rendering the three decks takes about 10 seconds, which is why it is a
separate command rather than part of npm run dev.
| Command | What it does |
|---|---|
npm run dev |
Dev server with hot reload |
npm run talks |
Render the Quarto decks into public/talks/ |
npm run build |
Build the site into dist/ (decks must already be rendered) |
npm run build:all |
talks then build, what CI runs |
npm run preview |
Serve dist/ exactly as it will be deployed |
npm run check |
Type-check .astro and .ts files |
Editing a .qmd deck updates the talks listing live in npm run dev, and so
does editing _data/*.yml: the content loaders watch both. The deck itself
only changes after npm run talks, since that is Quarto's job.
src/
├── pages/ # routes
│ ├── index.astro # home: hero + featured packages
│ ├── ecosystem.astro # package grids, from _data/*.yml
│ ├── why.mdx # coming from Shiny
│ ├── getting-started.mdx
│ ├── design-system.astro # token & component showcase
│ ├── talks/index.astro # listing, built from the decks' frontmatter
│ ├── guides/ # listing + one route per guide
│ └── blog/ # listing + one route per post
├── content/ # the prose those routes render
│ ├── guides/*.mdx
│ └── blog/*.mdx
├── layouts/
│ ├── BaseLayout.astro # <head>, nav, footer, theme bootstrap
│ └── DocLayout.astro # prose + table of contents with reading progress
├── components/
│ ├── ds/ # copied verbatim from the design system, see its README
│ └── *.astro # the site layer
├── content.config.ts # loaders: talks, guides, blog, packages, partners
├── styles/
│ ├── tokens/*.css # the design system's tokens, verbatim
│ └── global.css # base + prose layer
├── scripts/ # shared behaviour (tab panels)
└── lib/ # rehype plugins
talks/ # Quarto revealjs decks (its own Quarto project)
├── _quarto.yml # renders into ../public/talks/
└── <slug>/index.qmd
_data/ # packages.yml, partners.yml
assets/ # hyperverse-revealjs.scss, used by the decks only
public/ # static assets + the rendered decks (gitignored)
Four kinds of content, told apart by what they are for and where they live.
| Kind | Lives in | Route | Sorted by | For |
|---|---|---|---|---|
| Navigation page | src/pages/*.mdx or .astro |
one per file | n/a | The structure of the site: why, get started, ecosystem. Rarely added. |
| Guide | src/content/guides/*.mdx |
/guides/<file> |
order |
A task, once you have something running. Revised over time, so it carries no date. |
| Post | src/content/blog/*.mdx |
/blog/<file> |
date |
Dated writing: a release, a decision, a lesson. Stays as written. |
| Post published elsewhere | src/content/blog/*.mdx with external |
none | date |
Written for ThinkR or anywhere else. Listed here, rendered there. |
The last one is the reason posts and guides are separate collections rather than one: an external post is still a post, it just points somewhere else.
Adding external: { url, publisher } changes four things at once, all in the
same direction: the listing and the feed link to the original, no local route is
generated so nothing competes with it in search results, and llms.txt names the
publisher so a model quoting the piece credits the right one.
Both are MDX in src/content/, and the route is generated for you. A guide goes
in guides/, a post in blog/; the filename becomes the URL.
---
title: "Monitoring errors in production"
description: "One sentence. Shown under the title, in the listing, and in meta tags."
order: 10 # guides only: how the listing is sorted
date: 2026-09-16 # posts only
draft: false # true hides it from routes, listings and the sitemap
tags: [glitchtipr]
---
import Callout from '../../components/Callout.astro';
Markdown as usual. Separate sections with `##`; do not add rules of your own,
the prose layer deliberately draws none.src/content/blog/writing-a-post.mdx is a draft template covering the rest.
A section only appears in the navbar once it has something published, so an empty Blog is never advertised.
For something that is not part of a section, add an .mdx file directly to
src/pages/ with layout: ../layouts/DocLayout.astro in its frontmatter, then
link it: the navbar (links in src/components/Nav.astro) is for pages a
visitor needs, the footer columns (columns in src/components/Footer.astro)
for everything else. /talks and /design-system live in the footer for that
reason.
Create talks/<slug>/index.qmd with the usual frontmatter:
---
title: "Hyperverse"
subtitle: "..."
author: "Arthur Bréant"
date: 2026-06-17
event: "Rencontres R 2026 à Nantes"
event-en: "Rencontres R 2026 in Nantes, France"
description: "..."
description-en: "..."
categories: [htmxr, plumber2]
langs: [fr, en]
image: img/cover.png
---Then npm run talks. The listing at /talks reads this frontmatter directly,
so there is no second place to update.
For a translation, add index-<lang>.qmd in the same folder and list the
language in langs. The loader checks which files actually exist on disk, so
the listing can never link to a deck that was not rendered.
Edit _data/packages.yml (or partners.yml for third-party packages):
- name: mypackage
desc: "Short description."
badge: In development
badge_class: badge-dev
github: https://github.com/hyperverse-r/mypackage
pkgdown: ~badge_class accepts badge-cran, badge-dev or badge-planned. Cards render
in file order, so put the important ones first. Use ~ for "no link".
Self-hosted in public/fonts/, declared in src/styles/tokens/fonts.css. This
is the one file that deliberately diverges from the design system, which uses an
@import from Google Fonts: right for a design canvas, wrong for a site, since
an @import inside a stylesheet cannot start until that stylesheet has parsed.
Only weights that exist and are used. Lato has no 500 or 600 (the family is 100/300/400/700/900), so asking for them fetched nothing.
Watch what a page actually downloads before adding a weight. A single <b> in
the hero was pulling 30 KB of JetBrains Mono 700 for two digits; giving it the
600 already on the page removed a whole file from every visit.
To regenerate after changing a family or weight:
# 1. ask Google for the CSS as a modern browser would see it
curl -s -A 'Mozilla/5.0 ... Chrome/120' \
'https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap' > /tmp/gf.css
# 2. download each latin / latin-ext woff2 it lists into public/fonts/
# 3. copy its @font-face blocks into src/styles/tokens/fonts.css, swapping the
# remote src for /fonts/<file>.woff2 and keeping the unicode-rangeKeep font-display: swap and the unicode-range: the range is what stops a
visitor reading English from ever fetching the latin-ext file.
Every page carries a canonical URL, a description, Open Graph and Twitter cards
pointing at /og.png, and JSON-LD. Guides and posts declare themselves as
TechArticle; everything else is the site's WebSite schema, which also
describes hyperverse as SoftwareSourceCode so a search engine can tell what
the project is.
| Endpoint | What it is |
|---|---|
/sitemap-index.xml |
Generated. Drafts and redirects are excluded. |
/robots.txt |
Points at the sitemap; keeps crawlers out of the multi-megabyte decks. |
/rss.xml |
Blog posts only. A guide is revised, not announced. |
/llms.txt |
A plain-text map of the site for language models, per llmstxt.org. |
/llms-full.txt |
Every page as one plain-text document, MDX components unwrapped. |
The last three are generated from the same collections the site renders, so they
cannot drift from it. /og.png is the exception: it is a committed image, built
from an HTML card. Re-shoot it if the wordmark or the tagline ever changes.
The visual language comes from a Claude Design project.
src/styles/tokens/ and src/components/ds/ are copied from it unmodified:
do not hand-edit them, change the design system and re-sync. Everything else in
src/components/ is the site layer, written against those tokens.
Every colour, space, radius and duration is a --hv-* custom property. Dark is
the default; [data-theme="light"] flips it. /design-system renders the whole
set live, so it cannot drift from the tokens.
The components are authored against React; the site runs Preact in compat
mode, so they work unchanged on a much smaller runtime. Only the hero is
hydrated. Everything else in src/components/ is a .astro port that emits the
same DOM with CSS instead of component state. See src/components/ds/README.md
for which is which, and why.
Two rules worth knowing before touching the styles:
- Vampiro One is for the wordmark and the hero
h1only. Everything else,h2included, is Lato. - Code panels never flip to light. They use the always-dark
--hv-code-*family in both themes, because code reads better on a dark ground. - The glow has no light-theme value upstream.
HeroBackdrop.astrodefines its own, because the design system's pale violet at 0.06–0.10 alpha is invisible on#f8f8f8.
Copied into the build untouched, and therefore served: anything placed here is
public, including notes. hyperverse.svg, assets/, og.png, robots.txt and
CNAME live there; talks/ is written by npm run talks and is gitignored.
Never put a redirect file here for a URL whose stem matches a real route. A file
named talks.html shadows the /talks route on any host that resolves /talks
to talks.html, and a redirect back to /talks then loops forever. Old URLs are
handled by redirects in astro.config.mjs.
.github/workflows/publish.yml renders the decks, builds the site and pushes
dist/ to the gh-pages branch. The custom domain comes from public/CNAME.
Deck output is byte-for-byte reproducible for a given Quarto version, so
gh-pages does not grow when nothing changed, which is why the Quarto version
is pinned in the workflow rather than floating.