npm workspaces monorepo that builds both JellyRock documentation sites from
markdown that lives in jellyrock/jellyrock.
| Site | URL | Source markdown |
|---|---|---|
| User docs | docs.jellyrock.app | jellyrock/jellyrock → docs/user/ |
| Dev docs | dev.jellyrock.app | jellyrock/jellyrock → docs/dev/ |
Both sites share an identical build pipeline, shared UI, and theme — they differ only in content source, home page splash, and page order.
| Piece | Choice | Why |
|---|---|---|
| Framework | Astro 7 + Starlight | First-class docs features: sidebar, search, dark mode, TOC. |
| Markdown extensions | remark-github-alerts |
Lets > [!NOTE] / > [!TIP] / > [!CAUTION] render on both GitHub and Starlight. |
| Link rewriting | Custom shared/remark-upstream-links.mjs |
Upstream links written for GitHub (page.md, ../../scripts/x.js) work on the sites. |
| Link checking | starlight-links-validator |
A link to a missing page or #anchor fails the build. |
| Shared UI | jellyrock/shared-ui |
One nav/footer/tokens source for all three JellyRock sites. |
| Content pipeline | Custom shared/fetch-docs.mjs |
Shallow-clones the app repo at build time so devs can edit markdown next to the code. |
| Theme | Auto-generated from jellyrock/settings.json |
CSS variables mirror the app's in-product themes. |
| Analytics | Umami (self-hosted) | Privacy-respecting; one website ID per site (see each site's astro.config.mjs). |
| Hosting | Caddy file_server on the JellyRock VPS |
Same pipeline as the homepage. |
push to jellyrock/jellyrock main (notify-docs.yml)
│
└─▶ repository_dispatch (docs-update) ─┐
▼
push to jellyrock/docs (main) ─▶ .github/workflows/deploy.yml
│
├─ gates: ci.yml (npm ci, npm run check, build, browser tests)
│
├─ build:user build:dev
│ ├─ fetch-shared-ui.mjs ├─ fetch-shared-ui.mjs
│ ├─ fetch-docs.mjs('user') ├─ fetch-docs.mjs('dev')
│ │ ├─ shallow-clone app repo │ └─ reuses clone
│ │ ├─ copy docs/user/*.md │ ├─ copy docs/dev/*.md
│ │ ├─ regenerate theme.css │ ├─ regenerate theme.css
│ │ └─ write splash index.mdx │ └─ write splash index.mdx
│ └─ astro build → user-docs/dist/ └─ astro build → dev-docs/dist/
│
├─ browser tests (functional) on both builds
│
└─ rsync the tested dist/ → VPS:
/opt/jellyrock/user-docs/
/opt/jellyrock/dev-docs/
│
└─ Caddy serves the sites
docs/
├── user-docs/ # Starlight site for docs.jellyrock.app
│ ├── astro.config.mjs # site URL, Umami ID, nav, remark plugins
│ └── src/
│ ├── components/ # Starlight overrides (Header, Footer, PageFrame)
│ ├── content/ # populated at build time by fetch-docs.mjs
│ ├── shared-ui/ # populated at build time by fetch-shared-ui.mjs
│ └── styles/ # custom.css, theme.css (generated)
├── dev-docs/ # Starlight site for dev.jellyrock.app (same shape)
├── shared/
│ ├── fetch-docs.mjs # clones jellyrock, copies markdown, writes theme
│ ├── fetch-shared-ui.mjs # symlink (dev) or clone (CI) shared-ui
│ ├── remark-upstream-links.mjs # rewrites upstream relative links and images
│ └── templates/
│ ├── user-index.mdx # home page splash for docs.jellyrock.app
│ └── dev-index.mdx # home page splash for dev.jellyrock.app
├── scripts/
│ ├── build-warnings.sh # fails a build that prints an unexpected warning
│ ├── build-warnings-allowed.txt # warnings known to be harmless, each with its reason
│ └── playwright-docker.sh # runs Playwright in the image CI uses
├── tests/
│ ├── e2e/ # browser tests: one spec per site, screenshots, baselines
│ └── support/production-headers.json # the security headers Caddy sends
├── playwright.config.ts
└── .github/workflows/
├── ci.yml # PRs, and the gates every deploy runs first
└── deploy.yml
nvm use # Node from .nvmrc
npm install -g "$(node -p 'require("./package.json").packageManager')" # the npm CI and Renovate use
npm ci
npm run dev:user # http://localhost:4321 → docs.jellyrock.app
npm run dev:dev # http://localhost:4322 → dev.jellyrock.app
npm run build # builds both sites
npm run build:strict # same, and fails on a warning not in scripts/build-warnings-allowed.txt (CI runs this)
npm test # browser tests against both builds (see Browser tests)On the first build:
shared/fetch-shared-ui.mjsshallow-clonesjellyrock/shared-uiinto each site'ssrc/shared-ui/.shared/fetch-docs.mjssparse-clonesjellyrock/jellyrockinto.jellyrock-repo/at the repo root, then copies markdown into each site's content collection. Every build updates the clone to upstreammain(it logsUsing jellyrock/jellyrock <commit> <date>) and re-copies the content. The clone is a cache: edits made inside it are discarded on the next build, so edit docs in the jellyrock repo instead.
This is the same path CI uses — no setup required.
To see a docs change before it merges, point the build at a local checkout of
jellyrock/jellyrock (a worktree or a PR branch) instead of the clone:
JELLYROCK_SOURCE=/path/to/jellyrock npm run dev:userThe build reads that checkout as it is, uncommitted edits included, and logs
its path after the commit. The Edit page links still point at main. CI
never sets it.
If you're actively editing shared-ui and want edits to hot-reload in the docs
sites without a commit-push-rebuild cycle, clone it as a sibling of this repo:
git clone https://github.com/jellyrock/shared-ui.git ../shared-uiWhen ../shared-ui/ exists, fetch-shared-ui.mjs symlinks each site's
src/shared-ui/ to it instead of cloning. Vite picks up every save instantly.
Remove the sibling (or rm -rf user-docs/src/shared-ui dev-docs/src/shared-ui)
to go back to the clone-from-GitHub default.
Caution
The symlink points at your local working tree — you can ship a build
referencing shared-ui commits that aren't pushed yet. Always push
shared-ui before pushing the consumer sites that depend on the change.
Pages are authored in jellyrock/jellyrock:
- User docs →
jellyrock/jellyrock:docs/user/ - Dev docs →
jellyrock/jellyrock:docs/dev/
Use plain GitHub-flavored markdown. The title is inferred from the first H1 if
no frontmatter is present; fetch-docs.mjs injects title: + description:
frontmatter before Starlight sees the file.
Callouts use the GitHub syntax so they render on both GitHub and the docs site:
> [!NOTE]
> Helpful context.
> [!TIP]
> A recommended approach.
> [!WARNING]
> Something to watch out for.
> [!CAUTION]
> Don't do this.Links and images use GitHub-style relative paths, so they work when reading the
markdown on GitHub too. At build time
remark-upstream-links.mjs rewrites each one:
| Upstream link | On the site |
|---|---|
manage-subtitles.md#anchor (same docs folder) |
/manage-subtitles/#anchor |
../dev/debug-flags.md (the other docs folder) |
https://dev.jellyrock.app/debug-flags/ |
../../scripts/rta-run.js (any other repo file) |
https://github.com/jellyrock/jellyrock/blob/main/scripts/… |
 or <img src="images/…"> |
An optimized image (a raw <img> loses its width) |
The build fails on a link to a page or #anchor that doesn't exist, and on a
missing image, so a broken link blocks the docs deploy until it's fixed upstream.
Push to main and the docs sites redeploy within minutes: jellyrock/jellyrock's
notify-docs.yml sends docs-update here whenever a file the build reads changes
(the paths ensureClone() in fetch-docs.mjs checks out).
Broken links and anchors in docs/user and docs/dev fail that repo's
docs-check on the PR, before they reach this build.
When a user page moves upstream, add its old and new paths to MOVED_PAGES in
user-docs/astro.config.mjs, so links to the old
URL keep working. The old URL redirects only once no page is left there, so the
entry can land before or after the move.
The home page for each site is not sourced from jellyrock/jellyrock; it's a
local template that fetch-docs.mjs copies into src/content/docs/index.mdx
on every build:
- User splash →
shared/templates/user-index.mdx - Dev splash →
shared/templates/dev-index.mdx
These are Starlight MDX files — feature cards use the <Card> / <CardGrid> /
<LinkCard> components from @astrojs/starlight/components. Edit freely; the
template always overwrites whatever Starlight may have generated previously.
Starlight orders sidebar items alphabetically by filename by default. Two options:
-
Filename prefix (recommended, zero config). Rename files in
jellyrock/jellyrock:docs/user/ordocs/dev/with numeric prefixes:01-getting-started.md,02-configuring-server.md, etc.fetch-docs.mjspreserves filenames, and Starlight sorts them lexically. The prefix is hidden from the URL via the page's slug/frontmatter. -
Explicit sidebar in
astro.config.mjs. For fine-grained control (groups, labels, collapsed state), define asidebarin the Starlight integration options. See the Starlight sidebar docs.
Starlight renders plain HTML, so three paths work today:
-
Self-hosted MP4/WebM. Drop the file in
jellyrock/jellyrock:docs/screenshots/(already synced byfetch-docs.mjs) and embed with:<video src="/screenshots/my-demo.webm" controls muted playsinline width="720"> Your browser does not support the video tag. </video>
Keep files lean — no image pipeline optimizes video. Prefer WebM at 720p, <2 MB.
-
YouTube / Vimeo embed. Starlight supports
astro-embed— add it as a dependency and import<YouTube id="..." />in an.mdxpage. -
Animated GIFs. Work out of the box as
but tend to be large — prefer WebM.
Note
If video support becomes common, consider adding a <Video /> component to
jellyrock/shared-ui so both sites render videos consistently (lazy loading,
poster frames, etc.).
| Want to change | Where |
|---|---|
| Colors / typography tokens | jellyrock/shared-ui:tokens.css |
| Starlight-specific CSS overrides | user-docs/src/styles/custom.css and dev-docs/src/styles/custom.css |
| In-app color themes (cascades here) | jellyrock/jellyrock:settings/settings.json — fetch-docs.mjs regenerates theme.css from this |
| Header links / footer sections | jellyrock/shared-ui:nav.ts |
| Starlight site title, accent, social | user-docs/astro.config.mjs, dev-docs/astro.config.mjs |
Each site has its own website in the self-hosted Umami dashboard, wired up via
the head array in its astro.config.mjs.
CI opens both built sites in a real browser (Playwright, Chromium) at desktop and phone size before anything merges or deploys, so a dependency bump or an upstream docs change that breaks a page turns CI red instead of reaching the sites. Each build is served the way production serves it: the same security headers (including the CSP), redirects and 404 handling.
The server and the checks every JellyRock site shares come from shared-ui's
testing/, which
the build fetches into each site's src/shared-ui/testing/. This repo keeps
the config, the specs, the screenshots and the production headers.
| Test | Fails when |
|---|---|
tests/e2e/user-docs.spec.ts, dev-docs |
Any page in the sitemap has a script error, something the CSP blocks, a failed or cross-origin request, a broken image, sideways scrolling on a phone, or a missing header/footer; an internal link or anchor redirects or is broken; a missing page doesn't return 404 |
tests/e2e/screenshots.ts |
A landing page or 404 page looks different from its approved screenshot in tests/e2e/__screenshots__/ |
Screenshots cover only the pages this repo owns. Content pages come from
jellyrock/jellyrock main at build time and change there without a PR here,
so they get the functional checks only; the sidebar's page list, the
previous/next links and images are masked for the same reason. Deploys,
including the ones upstream docs edits trigger, run the functional tests but
not the screenshots: the theme colours come from upstream too, and a deploy
has no PR to refresh baselines in.
npm run build # the tests run against user-docs/dist and dev-docs/dist
npm test # everything, inside the same Playwright image CI uses
npm run test:e2e # functional tests only, with a local browser
# (first time: npx playwright install chromium)
npm run test:visual # screenshot tests only (Docker)
npm run test:visual:update # accept the current look as the new baseline (Docker)When CI goes red, open the failed run's playwright-report artifact and
its index.html: each failure shows the error, and each screenshot failure
shows expected, actual and a diff. A failure on a content page usually comes
from the upstream markdown: fix it in jellyrock/jellyrock.
After a deliberate design change (or a shared-ui, Starlight or upstream
theme change that changes how the pages look), the screenshot tests go red as
intended. Check the diff is what you meant, run
npm run build && npm run test:visual:update, and commit the updated PNGs in
the same PR.
Screenshots only match inside the Playwright image, because fonts render
differently between machines, which is why those commands run through Docker
(scripts/playwright-docker.sh). The image is
pinned in .github/workflows/ci.yml, and Renovate moves it together with
@playwright/test.
The production headers are a copy of what Caddy sends (its config lives in
jellyrock/infra), kept in
tests/support/production-headers.json.
Update it when the docs sites' headers change.
Required repo secrets (org-level, shared with sibling deploy repos):
DEPLOY_SSH_KEY— private key authorized on VPSjellyrock@VPS_KNOWN_HOSTS— pre-verifiedssh-keyscanoutputVPS_HOST,VPS_USER— deploy target
| Symptom | Likely cause | Fix |
|---|---|---|
Dev server serves stale theme.css |
Vite HMR didn't pick up the regenerated file | Restart npm run dev:* |
| Old content keeps showing in dev | Build warned it could not update the clone | Check your network, then restart npm run dev:* or rebuild |
| Deploy fails at rsync step | VPS_KNOWN_HOSTS secret missing or stale |
Refresh via ssh-keyscan on the control host, update the secret |
fetch-shared-ui.mjs clones instead of symlinking |
No sibling ../shared-ui/ present |
Expected in CI; for local dev, clone it as a sibling |
| Alerts render as plain blockquotes | remark-github-alerts missing or removed |
Check the markdown.remarkPlugins array in both astro.config.mjs files |
| A browser test fails only on a content page | The upstream markdown (an external image the CSP blocks, a wide table) | Open the playwright-report artifact; fix the page in jellyrock/jellyrock |
| CI fails with "Build warnings not in …" | A build warning that isn't on the allowed list | Fix the cause (often upstream docs or a dependency bump); if it's harmless, add it to scripts/build-warnings-allowed.txt with the reason |