Skip to main content

Site architecture

This site runs several independent documentation sets — two unversioned, plus one per plugin and library with its own version history — alongside a blog, all from a single Docusaurus install. This page explains how that is wired, so future-you can change it safely.

Docs instances​

The classic preset's built-in docs are switched off (docs: false) and replaced with explicit @docusaurus/plugin-content-docs instances. Two are fixed and unversioned:

Instance idContent folderServed atSidebar file
portfoliodocs/portfolio//portfolio/sidebarsPortfolio.ts
maindocs/main//docs/sidebarsMain.ts

Plus one versioned instance per project, generated from the PROJECTS array in src/data/projects.ts — see Versioned project docs.

There is no site-wide API instance

An earlier version of this site had one. It was removed: a Java API belongs to a specific library version, so it cannot live in an unversioned, site-wide section. Each project's API docs are a category inside its own versioned instance.

Each instance is fully separate: its own sidebar, its own "next/previous" navigation, and no cross-linking by doc id. To link between instances, use an absolute site path such as /plugins/project-one/ rather than a relative Markdown link.

Adding a non-project instance

A folder under docs/ does nothing on its own — it needs a matching plugin entry in docusaurus.config.ts and a sidebar file, and its routeBasePath must be added to the search plugin's docsRouteBasePath array. Miss the last step and the new section silently stays out of the search index.

Adding a project is different: one entry in src/data/projects.ts handles all of that. Prefer that route.

The root doc of each instance​

Each instance has one page with slug: / in its front matter, which makes it the landing page for that route (/portfolio/, /docs/, /<category>/<id>/). Removing that front matter turns the route into a 404.

Versioned project docs​

Each project in src/data/projects.ts becomes its own docs instance with its own independent version history. Everything is derived from that one array: the plugin instance, the navbar entry, the version dropdown, the footer links, the /plugins and /libraries listing pages, and the search index.

Two categories, one dropdown​

Projects are split into two categories, because a server owner looking for a jar and a developer looking for a dependency are not the same visitor:

CategoryRouteDocs folderListing page
Plugins/plugins/<id>/docs/plugins/<id>//plugins
Libraries/libraries/<id>/docs/libraries/<id>//libraries

The category is derived from kind by projectCategory() — plugin and plugin-api are both plugins — and all the labels, folders and routes come from CATEGORY_META. In the rest of this page, <category> means plugins or libraries.

Both live under a single Minecraft navbar dropdown, grouped with headings:

Minecraft ▾
PLUGINS
All plugins
Project One
─────────────
LIBRARIES
All libraries
paper-dispatch
Why one dropdown and not two levels

Docusaurus navbars do not support nested dropdowns. The group headings are type: 'html' items, which the theme renders as <li> inside the dropdown, so they are inert labels rather than links — the "All plugins" / "All libraries" entries are the real links. Styling lives in custom.css under .dropdownGroupLabel.

The frozen <id>_versioned_* items are keyed by project id, not category, so moving a project from one category to the other does not require renaming them.

Minecraft docs serve two audiences that want nothing in common: server owners, who install a jar and edit config.yml, and plugin developers, who add a dependency and call an API. Interleaving them is the most common failure in plugin documentation, so the kind field on each project picks one of three sidebars:

kindSidebar fileShape
pluginsidebarsPlugin.tsInstall → Configure → Use → Help
plugin-apisidebarsPluginApi.tsThe above, plus a collapsed Developer API
plugin-frameworksidebarsPluginFramework.tsplugin-api, plus Building with it before Integrations
librarysidebarsLibrary.tsGetting started → Guides → Reference → Migrating

The three plugin sidebars import PLUGIN_SIDEBAR_ITEMS from sidebarPluginItems.ts and add to it, so the plugin shapes cannot drift apart. The items live outside the sidebar files because Docusaurus treats every export of a sidebar file as a sidebar. plugin-framework is for a plugin other plugins and data packs build on (MachineEngine): its content-defining reference — YAML definitions, block and item config, menus — fits none of commands, permissions or placeholders, so it gets a category of its own (building/*).

Server-owner sections come first and the developer section last, because most visitors to a plugin page are server owners — and a developer will happily scroll past install instructions, while the reverse is not true.

The sidebars are explicit, not autogenerated. That is deliberate: every plugin then documents the same things in the same order, which is worth far more across several projects than per-project flexibility. The trade-off is that a new project must contain every document its sidebar lists, or the build fails.

Adding a project​

  1. Add an entry to PROJECTS in src/data/projects.ts, with the right kind.
  2. Copy the docs folder from an existing project of the same kind into docs/<category>/<id>/ and rewrite the content.
  3. Build. The project is live at /<category>/<id>/.

No config, CSS, navbar or sidebar edits.

Values that must not go stale​

Anything a reader could act on wrongly is derived from PROJECTS rather than typed into Markdown:

ComponentRenders
<DependencySnippet>Maven / Gradle / Gradle KTS snippets, JitPack coordinates
<CompatibilityTable>Supported server software and Minecraft versions
<JavadocLink>Link to the JitPack-hosted Javadoc
<DownloadLinks>Download buttons (Hangar, Modrinth, GitHub releases)

Bumping latestVersion in projects.ts updates every dependency snippet on the site at once. Hand-written coordinates go stale on the next release, and a wrong snippet is the fastest way to lose a developer.

Javadoc is a page rather than a sidebar type: 'link' item because sidebar files export a plain object and get no per-instance context — they cannot know which project's URL to use. The page also carries the caveat that JitPack's latest may be newer than the doc version being read.

Cutting a version​

npm run docusaurus docs:version:<projectId> 1.0

This snapshots the current docs and creates three items in the repository root — the location is fixed by Docusaurus and cannot be configured:

PathContents
<id>_versioned_docs/version-1.0/Frozen copy of the docs
<id>_versioned_sidebars/Frozen copy of the sidebar
<id>_versions.jsonOrdered list of version names

These are committed, not generated at build time. Expect the repository root to grow by three entries per project.

How versions map to URLs​

VersionURLMeaning
Newest numbered/<category>/<id>/What visitors get by default
Older numbered/<category>/<id>/1.0/Kept for people on that release
current (Next 🚧)/<category>/<id>/next/Unreleased work in docs/

So docs/<category>/<id>/ is always the unreleased version. Editing it does not change what visitors see at /<category>/<id>/ until you cut a new version.

Before the first version is cut

A project with no releases keeps current at the route root, so /<category>/<id>/ works from the moment you add it, and it gets no version dropdown — there would be nothing to switch between.

docusaurus.config.ts detects this by reading <id>_versions.json from disk; the current -> /next/ move is only applied once at least one release exists. Without that check, a brand-new project would serve its docs only at /<category>/<id>/next/, leaving every navbar and footer link to /<category>/<id>/ broken — and onBrokenLinks is throw, so the build would fail until you cut a version.

Automating the version cut​

docs:version:<id> is a command you have to remember at release time, and you will not. Left manual, every project's docs end up frozen at 1.0 while you ship 2.x — worse than no versioning, because the dropdown then actively lies.

.github/workflows/version-docs.yml cuts a version, verifies the site still builds, and commits the snapshot. Run it from the Actions tab, or have the project's own repository fire a repository_dispatch when it publishes a release — the sender workflow is in a comment at the top of that file.

It validates the project id and version against a strict pattern before using them, because both end up in file paths, and a dispatch payload is untrusted input. It also refuses to overwrite a version that already exists.

After a cut, bump latestVersion in src/data/projects.ts so the dependency snippets show the new release.

Unreleased (next) docs are deliberately excluded from search, so visitors cannot land on documentation for something you have not shipped.

Why the version dropdowns are scoped in CSS​

docsVersionDropdown navbar items render on every page — the theme component has no "only when this instance is active" mode. With one instance per project, every page would carry a row of identical dropdowns.

Docusaurus puts plugin-id-<instanceId> on the <html> element of docs pages. Since that is server-rendered, CSS can hide every dropdown and reveal only the active project's, correct on first paint with no flash. The rules are generated from PROJECTS in versionDropdownScopingCss (in docusaurus.config.ts) and inlined into <head>.

The theme puts the className on the dropdown's inner <a>, not on the .navbar__item wrapper, so the selectors use :has(). They are wrapped in @supports — a browser without :has() shows all dropdowns rather than hiding all of them.

Theming​

Styling is layered, cheapest override first:

  1. src/css/custom.css — overrides Infima's --ifm-* variables. This is where the brand colour scale and typography live. Prefer changing a variable over writing a new rule.
  2. Site tokens — --site-accent, --site-hero-gradient, --site-surface and friends, also in custom.css. Page-level CSS Modules read these so gradients and shadows are defined once.
  3. CSS Modules — *.module.css next to each component, for layout that only that component needs.

Infima needs all seven shades of --ifm-color-primary-*. Defining only some of them leaves buttons and hover states falling back to the default Docusaurus green. Generate a full scale with the Docusaurus colour tool.

Search is provided by @easyops-cn/docusaurus-search-local, which builds a static index at build time. No Algolia account, no network calls at runtime.

It only works in a production build — npm run start shows no results. Test it with:

npm run build && npm run serve

Deployment​

.github/workflows/deploy.yml builds on every push to main and publishes via GitHub Pages. ci.yml runs the same checks on pull requests, so a broken build is caught before it lands. Because onBrokenLinks is throw, a dead internal link fails the build rather than shipping.

The repository is a GitHub user site (1robie.github.io), so it is served from the domain root and baseUrl stays '/'. A project site would need baseUrl: '/<repo-name>/'.

Never run npm run deploy

docusaurus deploy force-pushes build output to a branch. On a <user>.github.io repository the conventional target is main — which is where this source lives. Running it can replace the source tree with generated HTML.

Deployment goes through the workflow, which uploads an artifact and force-pushes nothing. deploymentBranch is pinned to gh-pages purely so that an accidental run cannot reach main.

Migrating from branch-based Pages​

This repository was originally deployed the legacy way: Pages served built HTML from the main branch root. Switching to the Actions-based deploy means setting Settings → Pages → Source to GitHub Actions, and the order matters — if source lands on main while Pages still serves that branch, the live site 404s, because there is no index.html at the repository root.

Check which mode is active:

gh api repos/1robie/1robie.github.io/pages --jq '.build_type, .source'