Skip to content

Writing documentation

Moongate uses Astro Starlight for its English and Italian documentation. English keeps the existing root URLs; Italian authored pages use /it/. The public site is served by Codeberg Pages on the custom domain moongate.sh. Releases publish the site automatically, and the same workflow can be run by hand to publish documentation between releases.

Use Node 24.21.0 (also recorded in website/.nvmrc). From the repository root:

Terminal window
npm --prefix website ci
npm --prefix website run dev

Open the / URL printed by Astro. Development search is unavailable; use the production preview to check the search index.

Terminal window
npm --prefix website test
npm --prefix website run build
npm --prefix website run preview -- --host 127.0.0.1 --port 4321

Open http://127.0.0.1:4321/. The build imports the source documents, builds the site and search index, and validates local links, images, and fragments. External links are not fetched by the validator.

Keep the source text in its existing location:

  • docs/*.md contains server, reference, and contributor guides; the Lua guide is docs/scripting.md with its pages in docs/scripting/.
  • src/*/README.md contains library documentation and NuGet examples.
  • The root README.md supplies the overview.
  • website/src/content/docs/index.mdx is the authored landing page. Its download cards use website/src/components/Downloads.astro and the version in Directory.Build.props to link to the matching release archives.

Do not edit or commit website/src/content/docs/generated/ or website/public/generated/. The importer replaces these directories. It preserves the original files, including NuGet smoke-test markers and code examples.

The importer also copies scripts/install.sh to website/public/install.sh, which the site serves at https://moongate.sh/install.sh. Edit the script, never the copy.

The packet reference at /packets/ is an Astro route. npm run dev and npm run build regenerate website/src/generated/packets.json from the C# registry and attributed Ultima packet types, then merge it with website/packets/overrides.json. The generated JSON is ignored by Git. Each packet needs an override keyed by opcode:direction (for example 0xBD:incoming); subcommands use IDs such as 0xBF/0x08:outgoing. Build fails when either source has an entry the other lacks. The dump requires the .NET 10 SDK.

The Lua API reference at /lua/ is generated too: an overview, one page per module and a page of enums. npm run dev and npm run build run website/lua/dump, which lists what the server publishes to Lua: the modules and enums AddUltimaScriptModules registers and the modules of Moongate.Scripting, with each function’s signature read from its C# method. website/scripts/build-lua.mjs turns that list into Markdown under website/src/content/docs/lua/, which Git ignores. Nothing is written by hand: a module’s description comes from [ScriptModule], a function’s text from the helpText of [ScriptFunction], and the same description feeds moongate_root/scripts/definitions.lua, so the editor and the site agree. Build fails when a module has no description, a function has no help text, or a name repeats. Each function has an anchor named after it, such as /lua/npc/#walk_to. The dump requires the .NET 10 SDK.

An example goes in website/lua/examples/<module>.md: one ## <function> section per function, whose body, a sentence and a Lua code block or a note, is put under that function on the module’s page. The body is Markdown. Build fails for a file that names no module, a section that names no function of the module, a repeated or empty section, text before the first section, a file with no section, a code block left open, and any other heading inside a section. The test suite checks the names too (PublishedScriptModulesTests), so renaming a function that has an example fails a pull request.

Help text is written to the pages as HTML, not Markdown, so Lua such as g:text{...} or ~1_NAME~ prints as written. A constant read through a C# property, such as engine.version, shows “set when the server starts”: its value belongs to the process that reads it. The site is built at release time, so PublishedScriptModulesTests checks the same requirements in the test suite: a pull request that adds a function without help text fails there.

The Test coverage page is filled from the coverage report in artifacts/coverage (or MOONGATE_COVERAGE_DIR): the importer replaces the <!-- coverage-summary --> marker with the per-assembly table and copies the HTML report to website/public/coverage/, served at https://moongate.sh/coverage/. MOONGATE_COVERAGE_COMMIT names the measured commit on the page. Without a report the page says none was available. Run scripts/coverage.sh all first to preview it locally.

After editing an imported source while the dev server is running, run this in a second terminal:

Terminal window
npm --prefix website run prepare:docs

Astro watches the generated pages; there is no separate watcher for source files outside website/. The dev and production build commands also prepare pages first.

  1. Write the English Markdown source in docs/ or the library’s README.md.
  2. Add an entry to website/content-manifest.mjs with its repository-relative source path, unique slug, title, and navigation group.
  3. Run the tests and production build above.

Slugs use lowercase letters, digits, hyphens, and slash-separated segments. Use relative links to other repository documents or source files, and preserve heading fragments. Imported documents become site links, local images are copied, and other repository files and directories become Codeberg links at the published release tag. The first top-level title is removed from imported content because Starlight renders the title from the manifest; its original Markdown anchor or explicit HTML ID is retained. Existing absolute Codeberg links to imported documents on develop or main are also converted.

Missing source files, duplicate slugs, unresolved local links, and broken links in the built output fail the build. A failed import keeps the previous generated content and the authored homepage intact.

English sources remain in their existing locations. Italian translations live at website/translations/it/<manifest-slug>.md, with the full translated Markdown below a metadata comment on the first line:

<!-- translation: {"sourceHash":"<64-character SHA-256 of the English source>","title":"Italian page title"} -->
# Translated original heading
Translated content.

Calculate the source hash with sha256sum docs/<source>.md after reviewing the complete translation against that exact source. Preserve heading levels and order, all code blocks, API identifiers, commands, configuration keys, link destinations, and explicit HTML IDs. Translate prose, headings, table descriptions and link labels. Relative links are resolved from the English source location, not the translation folder. The importer supplies the original English heading anchors so existing fragments keep working.

Links between authored guides stay in the selected language. Lua API references, the packet registry, changelog and coverage reports always retain English content; do not add translations for these generated references. Lua examples and generated help text also stay in English. The Italian navigation links to the English references; the language selector on those references leads to the Italian homepage.

The landing pages are authored separately in website/src/content/docs/index.mdx and website/src/content/docs/it/index.mdx. Update both when changing the homepage. Sidebar group labels live in website/sidebar-translations.mjs; Starlight supplies the standard interface translations, supplemented by website/src/content/i18n/it.json for search messages and remaining labels. The custom download cards and footer contain English and Italian labels.

Run the strict catalog check before publishing translated content:

Terminal window
npm --prefix website run check:translations

It reports missing translations, outdated source hashes, changed code or links, and unexpected translation files. A normal build warns about stale translations and uses Starlight’s English fallback for missing or stale pages, with a visible notice. Never refresh a hash without reviewing and updating the translation. Invalid metadata or altered code/link structure fails the import without replacing the last successful generated output. Rerun prepare:docs after editing a translation while the development server is running.

After the initial documentation-only publication, automatic documentation builds and deployments run when the release workflow publishes a release tag. Commits to develop, ordinary main pushes, and pull requests do not build or publish the website.

The coverage page says that no report was available: the CI coverage artifact is not carried into the site build.

The docs job of .forgejo/workflows/release.yml runs after the release assets, the image and the chart are published. It builds the site with scripts/build-site.sh, showing the tag in the site title, and publishes website/dist with the git-pages action. The custom domain is moongate.sh; git-pages accepts the token of the workflow run because the zone of the domain has a _git-pages-forge-allowlist TXT record with the clone URL of the repository. The build and link checker share the root base path through website/site-config.mjs. Runs are serialized.

Each release replaces the current website; historical versions are not hosted. Local builds use develop for source links and read the current version from Directory.Build.props for the site title. To preview a release label and source ref, set MOONGATE_DOCS_VERSION and MOONGATE_DOCS_REF before running the build.

.forgejo/workflows/docs.yml also runs from the Actions tab, with Run workflow. Choose the branch to publish; the site is built from that branch, source links point at develop, and the title shows the version in Directory.Build.props unless release_tag gives another label.

Made with ❤️ by Tom

Buy me a coffee