Appearance
Previewing and building the docs site locally
This document is the canonical contributor guide for the laptop-only VitePress preview and build of the docs/ corpus. It maps the two opt-in make targets to the underlying VitePress binary, names the pinned tooling versions, walks through adding a new page to the sidebar, and points at the broader docs gates that already keep the corpus honest. The two targets are explicit opt-ins: they are deliberately not wired into make test or make lint so a contributor who never opens a markdown file never pays the npm-install cost.
The companion gate for this document is docs/contributing/ci.md, which catalogues the docs-related CI jobs (docs-check) and the make targets that reproduce them. This file is the operational read; ci.md is the gate-by-gate reference.
Prerequisites
- Node.js. The pinned Node version lives in the repo-root
.nvmrcand is the same one used by everynpm-driven CI job. Match it locally withnvm use --install "$(cat .nvmrc)"before running either Make target. - Pinned dependencies. The VitePress runtime and its peers are pinned in
tools/docs/package.jsonwith a committedtools/docs/package-lock.json. Both Make targets runnpm ciagainst that workspace before invoking the localnode_modules/.bin/vitepressbinary, so a fresh clone is sufficient and global npm state is never touched. - Disk layout. The build artefacts land in
bin/docs/and the VitePress on-disk cache lives underdocs/.vitepress/cache/— both are listed in the repo-root.gitignoreso a straygit addcannot commit them.
Run the preview
bash
make docs-previewmake docs-preview runs tools/docs/node_modules/.bin/vitepress dev docs and prints the local URL — the dev server listens on http://localhost:5174. Port 5174 is pinned in docs/.vitepress/config.ts (with strictPort: true) so the documented preview URL is always the actual URL and never silently falls back to another port. The server has hot-module reload wired up: editing any markdown file under docs/, the navigation config in docs/.vitepress/config.ts, or the sidebar entries causes the open browser tab to refresh in place. Stop the server with Ctrl+C; nothing is written outside docs/.vitepress/cache/.
The preview is the fast iteration loop while authoring. It does NOT fail on broken intra-doc links — that gate fires on the build (make docs-build) and on the CI docs-check job. Treat the preview as a render-truth view, not as a validation pass.
Build for guard runs
bash
make docs-buildmake docs-build runs tools/docs/node_modules/.bin/vitepress build docs --outDir <repo>/bin/docs and writes a static, deploy-ready bundle to bin/docs/. The build is the gate that surfaces broken intra-doc links and front-matter parse errors — exit-non-zero on either. Use it when:
- you are about to push a docs change and want a local mirror of what the
docs-checkCI job will catch; - you are running the
tests/docs/vitepress_build_smoke_test.gobuild-tag-gated integration test (go test -tags integration_docs ./tests/docs/...); - you want a static
bin/docs/tree to inspect with a plain HTTP server (e.g.python3 -m http.server -d bin/docs).
The output directory is excluded from git. All three docs targets (docs-build, docs-preview, docs-publish) are deliberately not part of the default make test or make lint chain — see internal/platform/make/make_targets_test.go for the drift gate that enforces that contract.
Publish to the SFTP webspace
bash
make docs-publishmake docs-publish rebuilds the site (it depends on docs-build, so you never ship a stale bin/docs/) and then rsyncs the three public surfaces to the SFTP webspace that fronts the plexsphere site. The remote layout is fixed by the hosting account and is not created by the target — only its contents are synced:
| Remote path | Source |
|---|---|
$(SFTP_REMOTE_DIR)/docs/ | bin/docs/ — the rendered VitePress bundle |
$(SFTP_REMOTE_DIR)/api/ | contrib/api/index.html + api/openapi/plexsphere-v1.yaml |
$(SFTP_REMOTE_DIR)/main/ | contrib/main/ — the standalone landing page |
Each upload runs rsync with --delete over an SSH transport, so the remote tree is made byte-equal to its local source — a removed page or a renamed spec path cannot linger as a stale, still-reachable URL. The api/ payload is assembled in a bin/publish-api/ staging directory first: the Redoc viewer loads the spec through the relative URL ./plexsphere-v1.yaml, so the HTML and the YAML must land in the same remote directory and a single --delete pass must see both.
The connection defaults to the production webspace. Override any of SFTP_USER, SFTP_HOST, SFTP_REMOTE_DIR, or SFTP_SSH_PORT on the command line to push to a staging target, e.g.:
bash
make docs-publish SFTP_HOST=staging.example.net SFTP_REMOTE_DIR=plexsphere-stagingSSH key authentication is expected — the target invokes rsync -e ssh non-interactively and does not prompt for a password, so configure the webspace SSH key in your agent (or ~/.ssh/config) before running it.
Publish everything: the site and the CLI downloads
bash
make publishmake publish is the umbrella release-to-webspace entrypoint. It runs make docs-publish as a prerequisite — so the three website surfaces above (docs/, api/, main/) ship exactly as documented — and then adds a fourth surface: the prebuilt plexctl operator CLI an operator downloads from the webspace get/ directory. For that surface the target:
- cross-compiles
cmd/plexctl/for every entry inPLEXCTL_PLATFORMS(linux/amd64,linux/arm64,darwin/amd64,darwin/arm64, andwindows/amd64by default) withCGO_ENABLED=0, intobin/publish-get/plexctl-<os>-<arch>(with a.exesuffix for Windows); - writes a
SHA256SUMSmanifest over the binaries so a download can be verified before it is trusted; - stages the static install page
contrib/get/index.htmlnext to them; and rsyncs the staging directory to$(SFTP_REMOTE_DIR)/get/with the same--delete+ SSH transport the other surfaces use, so a binary for a platform you removed fromPLEXCTL_PLATFORMScannot linger as a stale, still-downloadable URL.
| Remote path | Source |
|---|---|
$(SFTP_REMOTE_DIR)/get/ | cross-compiled plexctl-<os>-<arch> binaries, the SHA256SUMS manifest, and contrib/get/index.html |
The install page links to each binary with a relative URL — the same host-agnostic pattern the Redoc viewer uses to load the OpenAPI spec — so it renders correctly no matter which domain or path the get/ directory is served under.
Because publish both cross-compiles Go binaries and rebuilds the docs site, it needs a Go toolchain matching /.go-version in addition to the Node/VitePress prerequisites above. The SFTP_* overrides apply unchanged, and PLEXCTL_PLATFORMS can be overridden to trim or extend the matrix:
bash
make publish PLEXCTL_PLATFORMS="linux/amd64 darwin/arm64"Like the docs targets, make publish is an explicit operator entrypoint and is deliberately not part of the default make test or make lint chain.
Directory layout (docs/.vitepress/)
text
docs/
├── .vitepress/
│ ├── config.ts # navigation + sidebar + Vite resolution overrides
│ └── cache/ # dev-server cache (gitignored)
├── tutorials/ # Tutorials quadrant (learning-oriented lessons)
├── how-to/ # How-to guides quadrant (task-oriented runbooks)
├── reference/ # Reference quadrant (typed surfaces: api, cli, …)
├── architecture/ # Explanation quadrant — system map
├── contexts/ # Explanation quadrant — bounded-context model
├── explanation/ # Explanation quadrant — root + the Diátaxis meta page
├── contributing/ # Contributing (non-Diátaxis): this guide lives here
├── index.md # Home — the Diátaxis compass
├── tutorials/set-up-local-plexsphere.md # Tutorials — set up your local plexsphere
└── contributing/dev-stack.md # Contributing — dev stack runbookThe single source of truth for navigation is docs/.vitepress/config.ts. It declares the site title, the description, the Diátaxis-aligned top-level sidebar groups, and the Vite resolution overrides that let the build find the pinned runtime under tools/docs/node_modules/. The sidebar is hand-curated: there is no front-matter scrape and no auto-grouping in v1. A new file under docs/**/*.md is invisible to the preview until it has a sidebar entry, which is enforced by the drift gates under tests/docs/.
The canonical sidebar groups are the four Diátaxis quadrants in reading order, followed by Contributing — a deliberately non-Diátaxis fifth group for the contributor handbook. In the order they must appear in docs/.vitepress/config.ts, they are:
- Tutorials
- How-to guides
- Reference
- Explanation
- Contributing
The Explanation quadrant spans two page families — the system architecture under architecture/ and the bounded-context model under contexts/ — nested as sub-groups under the single Explanation group, with explanation/ carrying the quadrant root and the Diátaxis meta page.
Reordering or dropping any of these groups breaks the tests/docs/vitepress_config_test.go gate. Adding a new top-level group requires a coordinated update to that test, the config, and this guide in the same change.
Add a page to the sidebar (worked example)
Suppose you are adding a new how-to page at docs/how-to/operate-credentials.md. The minimum steps are:
Create the markdown file with the canonical front-matter block. At minimum the file declares
title,description,status,audience, andquadrant— the five fields the frontmatter gate intests/docs/frontmatter_contract_test.goenforces. For a how-to that isquadrant: how-to. See Docs authoring conventions for the full field table.yaml--- title: Operating credentials in the field description: How to rotate, revoke, and re-mint credentials from the field without breaking the in-flight session. status: draft audience: operator quadrant: how-to ---Open
docs/.vitepress/config.tsand add one entry under theHow-togroup'sitems: [...]. The entry shape is{ text: '<menu label>', link: '<path-without-extension>' }, matching the convention every other sibling already uses:ts{ text: 'Operate Credentials', link: '/how-to/operate-credentials' },Run
make docs-previewand confirm the new entry appears in the sidebar under How-to in the order you added it. Click through to verify the page renders.Run
make docs-buildto confirm the build is clean — no broken links, no front-matter parse errors. If the build is green, thedocs-checkCI job will be green for the same diff.If the new file links to (or is linked from) sibling docs, run
make docs-checkto verify markdownlint and lychee accept the prose and the relative link targets.
Cross-references
docs/contributing/ci.md— the CI pipeline operator guide. Lists thedocs-checkjob that gates every PR, the lychee / markdownlint commandsmake docs-checkruns, and how to reproduce a reddocs-checkrun locally.docs/contributing/testing.md— the test pyramid and the sharedinternal/platform/testutilharness, including the Feature-ID rule the docs gates inherit.docs/.vitepress/config.ts— the authoritative navigation + sidebar config the preview and build both consume.tests/docs/vitepress_config_test.go— the drift gate that enforces the four Diátaxis quadrants (plus the Contributing group) in canonical order.tests/docs/vitepress_build_smoke_test.go— the build-tag-gated integration test that runsmake docs-buildend-to-end and asserts a broken intra-doc link surfaces as a non-zero exit.