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.
Run locally
Section titled “Run locally”Use Node 24.21.0 (also recorded in website/.nvmrc). From the repository root:
npm --prefix website cinpm --prefix website run devOpen the / URL printed by Astro. Development search is unavailable;
use the production preview to check the search index.
npm --prefix website testnpm --prefix website run buildnpm --prefix website run preview -- --host 127.0.0.1 --port 4321Open 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.
Edit a page
Section titled “Edit a page”Keep the source text in its existing location:
docs/*.mdcontains server, reference, and contributor guides; the Lua guide isdocs/scripting.mdwith its pages indocs/scripting/.src/*/README.mdcontains library documentation and NuGet examples.- The root
README.mdsupplies the overview. website/src/content/docs/index.mdxis the authored landing page. Its download cards usewebsite/src/components/Downloads.astroand the version inDirectory.Build.propsto 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:
npm --prefix website run prepare:docsAstro watches the generated pages; there is no separate watcher for source files
outside website/. The dev and production build commands also prepare pages first.
Add a guide or library
Section titled “Add a guide or library”- Write the English Markdown source in
docs/or the library’sREADME.md. - Add an entry to
website/content-manifest.mjswith its repository-relative source path, unique slug, title, and navigation group. - 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.
Translate authored content
Section titled “Translate authored content”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:
npm --prefix website run check:translationsIt 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.
Release publication
Section titled “Release publication”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.
Publish between releases
Section titled “Publish between releases”.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
