diff --git a/.env b/.env
index 4174c07f2..4550c22ff 100644
--- a/.env
+++ b/.env
@@ -12,7 +12,7 @@ PROJECT_NAME="airstack"
# If you've run ./airstack.sh setup, then this will auto-generate from the git commit hash every time a change is made
# to a Dockerfile or docker-compose.yaml file. Otherwise this can also be set explicitly to make a release version.
# auto-generated from git commit hash
-VERSION="0.20.6"
+VERSION="0.20.7"
# Image-tag discriminator ONLY (appears in the image tag suffix, e.g. ..._robot-x86-64_dev).
# No Dockerfile consumes it: "prebuilt" does NOT bake the built ros_ws into the image today —
# a real prebuilt (workspace-baked) stage is future work. Keep "dev" (mounted code, built live).
diff --git a/.github/workflows/deploy_docs_from_develop.yaml b/.github/workflows/deploy_docs_from_develop.yaml
index f312c9d1b..4944f7c27 100644
--- a/.github/workflows/deploy_docs_from_develop.yaml
+++ b/.github/workflows/deploy_docs_from_develop.yaml
@@ -108,6 +108,10 @@ jobs:
git config --global user.name "Docs Deploy"
git config --global user.email "docs.deploy@example.co.uk"
- name: Build Docs Website
+ env:
+ # "Edit this page" links on the develop docs edit the develop
+ # branch (mkdocs.yml defaults edit_uri to edit/main/).
+ DOCS_EDIT_URI: edit/develop/
run: |
VERSION=$(grep -m1 '^VERSION=' .env | cut -d= -f2- | tr -d '"')
mike deploy --push --title "${VERSION} (unstable)" develop
diff --git a/.github/workflows/deploy_docs_from_main.yaml b/.github/workflows/deploy_docs_from_main.yaml
index 232d75285..432bf1aca 100644
--- a/.github/workflows/deploy_docs_from_main.yaml
+++ b/.github/workflows/deploy_docs_from_main.yaml
@@ -8,6 +8,7 @@ on:
- "stacks/**"
- "tools/gen_docs_catalog.py"
- ".github/workflows/deploy_docs_from_main.yaml"
+ - ".github/workflows/templates/root-redirect.html"
branches:
- main
# Serialize gh-pages pushes: concurrent docs deploys race on the branch
@@ -74,8 +75,11 @@ jobs:
mike deploy --push --update-aliases --title "${VERSION}" "${SLUG}" main
# Root URL redirects to the PINNED slug (not the moving alias): URLs
# people land on and copy stay valid across future releases. /main/
- # remains as a moving alias for deep links.
- mike set-default "${SLUG}" --push
+ # remains as a moving alias for deep links. The custom template adds
+ # a real
and Open Graph tags so a shared docs.theairlab.org
+ # link previews properly instead of as "Redirecting" (mike renders
+ # it with {{href}} = the slug).
+ mike set-default "${SLUG}" --push -T .github/workflows/templates/root-redirect.html
# mike re-sorts versions.json on every deploy, so pin develop — the
# higher, unreleased version — back to the top of the version selector
# after each deploy.
diff --git a/.github/workflows/templates/root-redirect.html b/.github/workflows/templates/root-redirect.html
new file mode 100644
index 000000000..a5a499eb0
--- /dev/null
+++ b/.github/workflows/templates/root-redirect.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ AirStack Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Redirecting to {{href}} ...
+
+
diff --git a/docs/hooks/social_meta_redirect_stub.py b/docs/hooks/social_meta_redirect_stub.py
new file mode 100644
index 000000000..389292f7d
--- /dev/null
+++ b/docs/hooks/social_meta_redirect_stub.py
@@ -0,0 +1,73 @@
+"""Add link-preview metadata to the version-root redirect stub.
+
+With ``docs_dir: .`` the site's version root (e.g. ``/0.20/``) is a bare
+redirect page generated by mkdocs-redirects (``index.md`` -> ``docs/index.md``)
+titled "Redirecting...". Link-preview scrapers (Slack, WhatsApp, iMessage, ...)
+read that page's raw HTML and neither run JavaScript nor follow meta-refresh,
+so a shared version-root URL previews as just "Redirecting...".
+
+This hook rewrites the generated ``/index.html`` after the build:
+a real title plus the same Open Graph / Twitter Card block the theme override
+(docs/overrides/main.html) puts on regular pages. MkDocs runs hooks after
+plugins for each event, so the redirect stub already exists on_post_build.
+"""
+
+import logging
+import os
+import re
+
+log = logging.getLogger("mkdocs.hooks.social_meta_redirect_stub")
+
+TITLE = "AirStack Documentation"
+DESCRIPTION = (
+ "An open-source ROS 2 autonomy stack for aerial robots — simulation, "
+ "ground control, and layered onboard autonomy that launch as one system. "
+ "By the AirLab at Carnegie Mellon University."
+)
+# The moving /main/ alias keeps this absolute URL valid across releases
+# (mike copies real asset files into alias directories).
+IMAGE = "https://docs.theairlab.org/main/docs/assets/media/splash-poster.jpg"
+
+META_BLOCK = f"""\
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+
+def on_post_build(config, **kwargs):
+ index_path = os.path.join(config["site_dir"], "index.html")
+ if not os.path.isfile(index_path):
+ log.warning("site index.html not found; skipping social meta injection")
+ return
+ with open(index_path, encoding="utf-8") as f:
+ html = f.read()
+ if "Redirecting" not in html:
+ # Not the redirect stub this hook expects (e.g. the root became a
+ # real page) — leave it alone.
+ log.info("site index.html is not a redirect stub; skipping")
+ return
+ html, n_title = re.subn(
+ r"[^<]* ", f"{TITLE} ", html, count=1
+ )
+ html, n_head = re.subn(
+ r"", META_BLOCK + "", html, count=1, flags=re.IGNORECASE
+ )
+ if not (n_title and n_head):
+ log.warning(
+ "could not inject social meta into redirect stub "
+ "(title replaced: %d, head found: %d)", n_title, n_head
+ )
+ return
+ with open(index_path, "w", encoding="utf-8") as f:
+ f.write(html)
+ log.info("injected social meta into version-root redirect stub")
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
new file mode 100644
index 000000000..7e7603d40
--- /dev/null
+++ b/docs/overrides/main.html
@@ -0,0 +1,37 @@
+{% extends "base.html" %}
+
+{#-
+ Open Graph / Twitter Card tags for link previews (Slack, WhatsApp,
+ iMessage, ...). Material only emits these via its `social` plugin, which
+ we don't run — this block provides them statically on every page.
+ home.html extends this template, so the whole site inherits the tags.
+ og:image uses the moving /main/ alias (mike copies real asset files into
+ alias dirs) so the absolute URL stays valid across releases.
+ og:url is deliberately omitted: site_url doesn't include the mike version
+ slug, so a computed canonical would point at a non-existent path.
+-#}
+{% block extrahead %}
+ {% if page and page.meta and page.meta.title %}
+ {% set og_title = page.meta.title ~ " - " ~ config.site_name %}
+ {% elif page and page.title and not page.is_homepage %}
+ {% set og_title = page.title ~ " - " ~ config.site_name %}
+ {% else %}
+ {% set og_title = config.site_name %}
+ {% endif %}
+ {% if page and page.meta and page.meta.description %}
+ {% set og_description = page.meta.description %}
+ {% else %}
+ {% set og_description = config.site_description %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/docs/release_notes/index.md b/docs/release_notes/index.md
index 983632cb7..5cc036570 100644
--- a/docs/release_notes/index.md
+++ b/docs/release_notes/index.md
@@ -21,6 +21,32 @@ its own notes. -->
- Nothing yet.
+## 0.20.7 — 2026-08-30
+
+- **Fixed 404s on every README-sourced docs page.** The `exclude_docs`
+ pattern intended to hide the repo-root `README.md` used gitignore
+ semantics, so it silently excluded *every* `README.md` in the tree —
+ 404ing the System Test Suite (`/tests/`), OSMO Lab Admin Guide
+ (`/osmo/`), and all autonomy package reference pages. The pattern is
+ now anchored to the repo root (`/README.md`).
+- **Shared docs links now show real previews.** Sharing
+ `docs.theairlab.org` in Slack/WhatsApp/iMessage previewed as
+ "Redirecting" — preview scrapers don't run JavaScript, and the site
+ root, the version root (`/0.20/`), and every content page lacked
+ Open Graph metadata. The root redirect now deploys from a template
+ with proper title/description/image tags (`mike set-default -T`), a
+ build hook enriches the version-root redirect stub, and a theme
+ override (`docs/overrides/main.html`) plus `site_description` add
+ per-page Open Graph / Twitter Card tags across the site. Note:
+ previously shared links may keep the stale preview until the apps'
+ link caches expire.
+- **"Edit this page" button on every docs page.** Each page now shows a
+ pencil icon (Material `content.action.edit` + `edit_uri`) linking to
+ GitHub's editor for that page's source file; contributors without
+ write access get GitHub's fork-and-propose flow. Pages built from
+ `main` link to `edit/main/`, the develop docs to `edit/develop/`
+ (via `DOCS_EDIT_URI` in the develop deploy workflow).
+
## 0.20.6 — 2026-08-29
- **Release notes now carry a dated section per hotfix.** Added the
diff --git a/mkdocs.yml b/mkdocs.yml
index 28eaa2bcd..06db3374d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -2,12 +2,19 @@
copyright: Copyright © 2024 - 2030 AirLab CMU
docs_dir: .
site_name: AirStack
+site_description: >-
+ AirStack is an open-source ROS 2 autonomy stack for aerial robots from the
+ AirLab at Carnegie Mellon University — simulation, ground control, and
+ layered onboard autonomy that launch as one system.
site_dir: ../site
site_url: "https://docs.theairlab.org/docs/" # Trailing slash is recommended
exclude_docs: |
# The repo README is GitHub-facing; the site's home page is docs/index.md,
- # and same-dir would otherwise collide on the root URL.
- README.md
+ # and same-dir would otherwise collide on the root URL. Patterns are
+ # gitignore-style: the leading slash anchors this to the repo root only —
+ # a bare README.md would exclude every package/tests/osmo README the nav
+ # serves as pages.
+ /README.md
**/ros_ws/build
**/docker/Foxglove
**/ros_ws/install
@@ -40,6 +47,9 @@ extra_css:
# build time, so each mike-deployed docs version shows only its own notes.
hooks:
- docs/hooks/release_notes_current_version.py
+ # Give the version-root redirect stub (/X.Y/ -> docs/) a real title and
+ # Open Graph tags so shared links preview properly (Slack, WhatsApp, ...).
+ - docs/hooks/social_meta_redirect_stub.py
markdown_extensions:
- admonition
- attr_list
@@ -243,6 +253,11 @@ plugins:
'docs/tutorials/index.md': 'docs/getting_started/tutorials_reference.md'
repo_name: castacks/AirStack
repo_url: https://github.com/castacks/AirStack
+# Per-page "Edit this page" pencil (theme feature content.action.edit).
+# docs_dir is the repo root, so page paths are already repo-relative.
+# GitHub shows contributors without write access its fork-and-edit flow.
+# The develop docs deploy overrides the branch via DOCS_EDIT_URI.
+edit_uri: !ENV [DOCS_EDIT_URI, "edit/main/"]
theme:
favicon: docs/assets/StackedWhite.png
custom_dir: docs/overrides
@@ -266,6 +281,7 @@ theme:
- content.code.copy
- content.code.annotate
- content.tooltips
+ - content.action.edit
font:
text: Inter
code: JetBrains Mono