diff --git a/docs/index.md b/docs/index.md
index a090386..6aafda1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -4,7 +4,7 @@ hide:
- toc
- feedback
template: home.html
-title: C/C++ Linting
+title: C/C++ Linter with clang-format and clang-tidy
description: clang-format and clang-tidy for C and C++ pull requests, pre-commit hooks and CI, with the LLVM version you pin.
---
diff --git a/docs/overrides/404.html b/docs/overrides/404.html
new file mode 100644
index 0000000..72a880c
--- /dev/null
+++ b/docs/overrides/404.html
@@ -0,0 +1,15 @@
+{% extends "main.html" %}
+
+{#-
+ Gives the 404 page a title of its own. With Material's default it was titled
+ "cpp-linter" like a real page, so missing pages were hard to tell apart in
+ Google Analytics.
+-#}
+
+{% block htmltitle %}
+
Page not found - {{ config.site_name }}
+{% endblock %}
+
+{% block content %}
+
404 - Not found
+{% endblock %}
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index 8701052..7875424 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -1,5 +1,34 @@
{% extends "base.html" %}
+{#-
+ Visits from the maintainers are tagged so that the Internal Traffic data
+ filter in Google Analytics can drop them. Opening any page with ?internal=1
+ stores a flag in localStorage, ?internal=0 removes it, and while the flag is
+ there every event carries traffic_type=internal. The docs under
+ /cpp-linter-action/, /clang-tools-pip/, /cpp-linter-rs/ and /cpp-linter/ are
+ on the same origin and read the same flag. It runs before Material's own
+ snippet, so the setting is queued ahead of the first page_view.
+-#}
+
+{% block analytics %}
+ {% if config.extra.analytics %}
+
+ {% endif %}
+ {{ super() }}
+{% endblock %}
+
{#-
The site's fonts are self-hosted in docs/fonts and declared in shared.css:
Instrument Sans for text, JetBrains Mono for code and Bricolage Grotesque for
@@ -7,10 +36,77 @@
Preloading the latin files for text and headings lets the browser fetch them
while it is still reading the stylesheets. The URLs must match the ones in
shared.css, and crossorigin is required for fonts even on the same origin.
+
+ The Open Graph and Twitter tags give links shared in chats and on social
+ sites a title, a description and the card in static/og-image.png. Pages with
+ no URL of their own, like the 404 page, leave og:url out.
-#}
{% block extrahead %}
{{ super() }}
+ {%- if page and page.meta and page.meta.title %}
+ {%- set og_title = page.meta.title %}
+ {%- elif page and page.title and not page.is_homepage %}
+ {%- set og_title = page.title | striptags %}
+ {%- else %}
+ {%- set og_title = config.site_name %}
+ {%- endif %}
+ {%- set og_description = (page.meta.description if page and page.meta and page.meta.description) or config.site_description %}
+ {%- set og_image = config.site_url ~ "static/og-image.png" %}
+
+
+
+
+ {%- if page and page.canonical_url %}
+
+ {%- endif %}
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{#-
+ Copying a code block is how a reader takes cpp-linter into a project, so each
+ copy is sent to Google Analytics as a copy_code event. snippet names the
+ section the block is in (the id of the nearest heading above it, or the
+ block's own id on the home page), and code_title is the file name shown above
+ the block, when it has one.
+-#}
+
+{% block scripts %}
+ {{ super() }}
+ {% if config.extra.analytics %}
+
+ {% endif %}
{% endblock %}
diff --git a/docs/robots.txt b/docs/robots.txt
new file mode 100644
index 0000000..873508b
--- /dev/null
+++ b/docs/robots.txt
@@ -0,0 +1,10 @@
+# The cpp-linter docs share this host: the organization site at the root and
+# each project's docs under its own path. This file covers all of them.
+User-agent: *
+Allow: /
+
+Sitemap: https://cpp-linter.github.io/sitemap.xml
+Sitemap: https://cpp-linter.github.io/cpp-linter-action/sitemap.xml
+Sitemap: https://cpp-linter.github.io/clang-tools-pip/sitemap.xml
+Sitemap: https://cpp-linter.github.io/cpp-linter-rs/sitemap.xml
+Sitemap: https://cpp-linter.github.io/cpp-linter/sitemap.xml
diff --git a/docs/static/og-image.png b/docs/static/og-image.png
new file mode 100644
index 0000000..1cd707f
Binary files /dev/null and b/docs/static/og-image.png differ
diff --git a/hooks/redirects.py b/hooks/redirects.py
new file mode 100644
index 0000000..1478a9e
--- /dev/null
+++ b/hooks/redirects.py
@@ -0,0 +1,61 @@
+"""Write redirect pages for old URLs that still get visits.
+
+GitHub Pages cannot send real redirects, so each old path gets a small page
+that forwards the browser with a meta refresh and tells search engines where
+the content lives now. The paths are the ones Google Analytics recorded as
+404s on this site: pages that moved into the sponsor page, short names for the
+getting started guide, and doc pages of cpp-linter-action, cpp-linter and
+cpp-linter-rs requested without their project prefix.
+
+A key ending in .html is written as that file; any other key is a directory
+and gets an index.html. Targets are relative to site_url.
+"""
+
+from __future__ import annotations
+
+import html
+from pathlib import Path
+
+REDIRECTS = {
+ "docs/": "getting-started/",
+ "merch/": "sponsor/#t-shirt-perk",
+ "inputs-outputs/": "cpp-linter-action/inputs-outputs/",
+ "examples/": "cpp-linter-action/examples/",
+ "permissions/": "cpp-linter-action/permissions/",
+ "pr-review-caveats/": "cpp-linter-action/pr-review-caveats/",
+ "cli_args/": "cpp-linter/cli_args.html",
+ "cpp_linter_rs/": "cpp-linter-rs/",
+ "cpp_linter_rs/cli.html": "cpp-linter-rs/cli/",
+}
+
+PAGE = """
+
+
+
+Redirecting to {url}
+
+
+
+
+
+
+