diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 56ccf964..cc23e74a 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -1,7 +1,9 @@
# Publishes the website, website/, on GitHub Pages: what OpenReliant is, the latest release's
# downloads, the roadmap and the mods, and the documentation under docs/ at /docs/. The page reads
# the release and the milestones from GitHub as it opens, so it needs publishing again only when
-# it or the documentation changes.
+# it or the documentation changes. Search engines find the pages through two sitemaps, the
+# website's own (website/sitemap.xml) and the docs' (docs/sitemap.xml), which robots.txt at the
+# root of openreliant.github.io names.
name: Pages
on:
@@ -26,7 +28,12 @@ jobs:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
+ # The whole history, without the old files' contents, for the day each page of the docs last
+ # changed (website/docs_hooks.py).
- uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ filter: blob:none
# The Predator's wire frame from the docs, drawn in the page's orange.
- run: sed 's/#7d8590/#f08a2c/g' docs/images/predator-wireframe.svg > website/predator.svg
# OpenReliant's icon (assets/icon) as the favicon of the site, its docs and the mods' site.
diff --git a/website/contribute/index.html b/website/contribute/index.html
index 6f1adafe..3abc71c2 100644
--- a/website/contribute/index.html
+++ b/website/contribute/index.html
@@ -5,6 +5,16 @@
Contribute · OpenReliant
+
+
+
+
+
+
+
+
+
+
diff --git a/website/docs-theme/main.html b/website/docs-theme/main.html
index 079d3adf..ebace76d 100644
--- a/website/docs-theme/main.html
+++ b/website/docs-theme/main.html
@@ -16,9 +16,24 @@
-{% if page.title and not page.is_homepage %}{{ page.title }} · {% endif %}OpenReliant docs
-
-{%- if page.canonical_url %}{% endif %}
+{#- The page's title and description, for the browser's tab and for search engines: a page's own
+ description is the start of its first paragraph (docs_hooks.py). #}
+{%- set title = page.title ~ " · OpenReliant docs" if page.title and not page.is_homepage else "OpenReliant docs: install, play and mod StarLancer" %}
+{%- set description = page.meta.description or config.site_description %}
+{{ title|e }}
+
+{%- if page.canonical_url %}
+
+
+{%- endif %}
+{#- What a link to the page shows in chats and on social sites. #}
+
+
+
+
+
+
+
diff --git a/website/docs-theme/sitemap.xml b/website/docs-theme/sitemap.xml
new file mode 100644
index 00000000..1d640466
--- /dev/null
+++ b/website/docs-theme/sitemap.xml
@@ -0,0 +1,15 @@
+{#- The docs' sitemap for search engines: each page's address, and the day git last changed its
+ file, where git says (docs_hooks.py). -#}
+
+
+{%- for file in pages %}
+ {%- if not file.page.is_link and file.page.canonical_url %}
+
+ {{ file.page.canonical_url|e }}
+ {%- if file.page.update_date %}
+ {{ file.page.update_date }}
+ {%- endif %}
+
+ {%- endif %}
+{%- endfor %}
+
diff --git a/website/docs_hooks.py b/website/docs_hooks.py
index e2c02db5..0148dfbc 100644
--- a/website/docs_hooks.py
+++ b/website/docs_hooks.py
@@ -1,12 +1,36 @@
-"""MkDocs hook for the website's docs: a link from a page under docs/ to a file outside it, such
-as a source file, goes to that file on GitHub, and one past the repository's root, such as
-../../milestones from the README, to that page of the repository on GitHub."""
+"""MkDocs hooks for the website's docs.
+- A link from a page under docs/ to a file outside it, such as a source file, goes to that file on
+ GitHub, and one past the repository's root, such as ../../milestones from the README, to that
+ page of the repository on GitHub.
+- Each page but the first gets a description for search engines and link previews: the start of
+ its text, as plain text (main.html).
+- Each page's date in the sitemap is the day git last changed its file (sitemap.xml), so search
+ engines see which pages changed. Without git's whole history, the sitemap gives no dates.
+"""
+
+import html
+import os
import posixpath
import re
+import subprocess
REPOSITORY = "https://github.com/OpenReliant/openreliant"
LINK = re.compile(r"(\]\()([^)\s]+)((?:\s+\"[^\"]*\")?\))")
+# A paragraph or an item of a list, and a tag within one.
+TEXT = re.compile(r"<(p|li)(?:\s[^>]*)?>(.*?)\1>", re.S)
+TAG = re.compile(r"<[^>]+>")
+
+# The longest description, about what a search result shows, and the shortest that is cut at the
+# end of a sentence rather than at a word. A description cut at a word ends in an ellipsis.
+LONGEST_DESCRIPTION = 160
+SHORTEST_DESCRIPTION = 80
+ELLIPSIS = "..."
+
+
+def on_pre_page(page, **_):
+ page.update_date = last_change(page.file.abs_src_path)
+ return page
def on_page_markdown(markdown, page, **_):
@@ -27,3 +51,51 @@ def rewrite(match):
return match.group(1) + url + (f"#{anchor}" if anchor else "") + match.group(3)
return LINK.sub(rewrite, markdown)
+
+
+def on_page_content(content, page, **_):
+ if not page.is_homepage and "description" not in page.meta:
+ text = ""
+ for match in TEXT.finditer(content):
+ text = " ".join((text + " " + html.unescape(TAG.sub("", match.group(2)))).split())
+ # A text that leads into a list goes on with its items.
+ if len(text) >= SHORTEST_DESCRIPTION and not text.endswith(":"):
+ break
+ if text:
+ page.meta["description"] = shortened(text)
+ return content
+
+
+def shortened(text):
+ """`text` as a description: whole where it fits, else cut after its last sentence that fits,
+ or at a word."""
+ if len(text) <= LONGEST_DESCRIPTION:
+ return text
+ end = text.rfind(". ", 0, LONGEST_DESCRIPTION) + 1
+ if end >= SHORTEST_DESCRIPTION:
+ return text[:end]
+ fits = text[: LONGEST_DESCRIPTION - len(ELLIPSIS)]
+ return fits[: fits.rfind(" ")].rstrip(",;:") + ELLIPSIS
+
+
+def git(*arguments):
+ """What git prints for `arguments`, run in the website's folder; None where it fails."""
+ try:
+ return subprocess.run(
+ ["git", *arguments], cwd=os.path.dirname(os.path.abspath(__file__)),
+ capture_output=True, text=True, check=True,
+ ).stdout.strip()
+ except (OSError, subprocess.CalledProcessError):
+ return None
+
+
+# Whether git has the repository's whole history. A shallow clone takes every file as changed in
+# its one commit.
+WHOLE_HISTORY = git("rev-parse", "--is-shallow-repository") == "false"
+
+
+def last_change(path):
+ """The day git last changed the file at `path`, such as 2026-10-24; None where git can't say."""
+ if not WHOLE_HISTORY:
+ return None
+ return git("log", "-1", "--format=%cs", "--", path) or None
diff --git a/website/index.html b/website/index.html
index cdc1e2ab..b87f056b 100644
--- a/website/index.html
+++ b/website/index.html
@@ -3,8 +3,45 @@
-OpenReliant
-
+OpenReliant: play StarLancer on Windows, macOS and Linux
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -38,7 +75,7 @@