Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ jobs:
fi
done

- name: Verify theme delivery in built sites
run: |
# --strict fails on broken links, not on a stylesheet or font
# that 404s. Assert the theme's assets actually reached each
# built site/ directory, reading extra_css and theme.logo from
# the resolved config so the check tracks mkdocs-base.yml.
for sample_dir in samples/*/; do
name="$(basename "$sample_dir")"
if [ -f "$sample_dir/mkdocs.yml" ]; then
echo "Verifying theme delivery for $name..."
python3 scripts/verify-theme-delivery.py \
--config "$sample_dir/mkdocs.yml" \
--site "site-ci/$name"
fi
done

build-pdf:
name: Build PDF
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ jobs:
run: |
mkdir -p site
cp samples/index.html site/
# This site deploys on every push to main, so it shows
# unreleased state no consumer can yet install. Stamp the built
# revision onto the landing page so an evaluator is not misled
# about what they are looking at.
SHORT_SHA="${GITHUB_SHA:0:7}"
COMMIT_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}"
sed -i \
"s|<span id=\"build-revision\">Preview build from the local working tree</span>|Built from <a href=\"${COMMIT_URL}\">${SHORT_SHA}</a> on $(date -u +%Y-%m-%d)|" \
site/index.html
for sample_dir in samples/*/; do
name="$(basename "$sample_dir")"
if [ -f "$sample_dir/mkdocs.yml" ]; then
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,22 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# The surface guard reads the previous tag's tree, so
# the job needs full history and the tags to resolve and
# read it. actions/checkout is shallow and tagless by
# default, which would let the guard pass while silently
# comparing against nothing.
fetch-depth: 0
fetch-tags: true

- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"

- name: Guard release surface
run: ./scripts/check-release-surface.sh "${{ steps.version.outputs.version }}"

- name: Extract release notes from CHANGELOG.md
id: release_notes
run: |
Expand Down
124 changes: 71 additions & 53 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,72 +1,90 @@
# Changelog

This file documents recent notable changes to this project. The format of this
file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
This file documents recent notable changes to this project. Within each
release, entries are grouped by the area a consumer's reviewer triages —
`manual`, `api-reference`, `shared`, and `scripts` — rather than by the
Keep a Changelog change-type headings, so a consumer reads only the
sections that affect its template. Version numbers follow the
`MAJOR.MINOR.PATCH` policy described in [the README](README.md#versioning).

## Unreleased

### Added
### manual

- Both `mkdocs-base.yml` files now carry a self-served browser identity:
an indigo palette with a light/dark toggle, `brand.svg` as the header
logo and favicon, and `theme.font: false` so no Google Fonts are
requested. `shared/styles/base.css` declares `@font-face` for the six
shipped woff2 files and sets `--md-text-font: "Pretendard"`, giving
Korean body typography served entirely from the site.
- The base `markdown_extensions` gained a Unicode-aware `toc.slugify`,
so Korean headings keep their characters in anchor ids, and a
`mermaid` custom fence under `pymdownx.superfences`, so a tagged block
renders as a `.mermaid` diagram. `base.css` also carries shared rules
that scroll wide tables horizontally and center Mermaid diagrams.
- Added `scripts/install-samples.sh`, which installs the theme into each
sample from the local checkout so the samples build through the same
`INHERIT` path a consumer uses. It takes a `--force` (`--clean`) flag
that drops each sample's generated `docs/theme/` before reinstalling,
so edits to a template or shared file are picked up instead of the
cached tree `fetch-theme.sh` would otherwise keep. `serve-samples.sh`
passes `--force` on every run so the preview always reflects edits.
- `mkdocs-base.yml` now carries a self-served browser identity: an indigo
palette with a light/dark toggle, `brand.svg` as the header logo and
favicon, and `theme.font: false` so no Google Fonts are requested.
- The base `markdown_extensions` gained a Unicode-aware `toc.slugify`, so
Korean headings keep their characters in anchor ids, and a `mermaid`
custom fence under `pymdownx.superfences`, so a tagged block renders as
a `.mermaid` diagram.
- Consuming projects now `INHERIT: docs/theme/mkdocs-base.yml` instead of
copying settings out of it. Because MkDocs replaces lists and scalars
wholesale on merge, `theme.features`, `markdown_extensions`,
`extra_css`, and `exclude_docs` are defined fully in the base and must
never be partially overridden by a consumer. The sample builds through
this path against a theme installed from the local checkout; its
`docs/theme/` tree is generated, not committed.
- `extra_css` now points at `theme/`.

### Changed
### api-reference

- Consuming projects now `INHERIT: docs/theme/mkdocs-base.yml` instead
of copying settings out of it. The base is the union of what every
consumer needs; because MkDocs replaces lists and scalars wholesale on
merge, `theme.features`, `markdown_extensions`, `extra_css`, and
`exclude_docs` must be defined fully in the base and never partially
overridden by a consumer. Both sample sites build through this path
against a theme installed from the local checkout; a sample's
- `mkdocs-base.yml` now carries a self-served browser identity: an indigo
palette with a light/dark toggle, `brand.svg` as the header logo and
favicon, and `theme.font: false` so no Google Fonts are requested.
- The base `markdown_extensions` gained a Unicode-aware `toc.slugify`, so
Korean headings keep their characters in anchor ids, and a `mermaid`
custom fence under `pymdownx.superfences`, so a tagged block renders as
a `.mermaid` diagram.
- Consuming projects now `INHERIT: docs/theme/mkdocs-base.yml` instead of
copying settings out of it. Because MkDocs replaces lists and scalars
wholesale on merge, `theme.features`, `markdown_extensions`,
`extra_css`, and `exclude_docs` are defined fully in the base and must
never be partially overridden by a consumer. The sample builds through
this path against a theme installed from the local checkout; its
`docs/theme/` tree is generated, not committed.
- `extra_css` now points at `theme/` and includes `api.css`.

### shared

- `styles/base.css` declares `@font-face` for the six shipped woff2 files
and sets `--md-text-font: "Pretendard"`, giving Korean body typography
served entirely from the site. It also carries shared rules that scroll
wide tables horizontally and center Mermaid diagrams.

### scripts

- Added `install-samples.sh`, which installs the theme into each sample
from the local checkout so the samples build through the same `INHERIT`
path a consumer uses. It takes a `--force` (`--clean`) flag that drops
each sample's generated `docs/theme/` before reinstalling, so edits to a
template or shared file are picked up instead of the cached tree
`fetch-theme.sh` would otherwise keep. `serve-samples.sh` passes
`--force` on every run so the preview always reflects edits.
- `fetch-theme.sh` now installs into `docs/theme/` instead of
`docs/.theme/`. MkDocs excludes every dot-prefixed path from the
build, so assets installed under the old path never reached the
published site.
`docs/.theme/`. MkDocs excludes every dot-prefixed path from the build,
so assets installed under the old path never reached the published site.
- `fetch-theme.sh` takes no arguments. The repository, template, and
version are read from `docs/theme.toml`, which the consuming project
commits. `--source <dir>` installs from a local checkout instead of a
release and records `source = "local"` in `docs/theme/.meta`.
- The install set gained `mkdocs-base.yml`, `shared/styles/base.css`,
and `build-docs-pdf.sh`, so consumers no longer keep their own copy
of the PDF script. `docs/theme/.meta` records the installed `repo`,
`version`, `template`, `digest`, and `source`; an unchanged tree is
left alone and an edited one is reinstalled. `docs/theme/` is the only
path the installer replaces: the tree is staged in a uniquely named
directory created beside it, so nothing else the project keeps under
`docs/` is removed.
- `build-docs-pdf.sh` takes an optional config path after the locale
and reads all cover text and the output filename from the config's
- The install set gained `mkdocs-base.yml`, `shared/styles/base.css`, and
`build-docs-pdf.sh`, so consumers no longer keep their own copy of the
PDF script. `docs/theme/.meta` records the installed `repo`, `version`,
`template`, `digest`, and `source`; an unchanged tree is left alone and
an edited one is reinstalled. `docs/theme/` is the only path the
installer replaces: the tree is staged in a uniquely named directory
created beside it, so nothing else the project keeps under `docs/` is
removed.
- `build-docs-pdf.sh` takes an optional config path after the locale and
reads all cover text and the output filename from the config's
`extra.pdf` block. `extra.pdf_copyright` is replaced by
`extra.pdf.copyright` and is now an error. The config it generates
for MkDocs goes to a uniquely named scratch file, so a project that
already has an `mkdocs.tmp.yml` no longer has it overwritten and
deleted. `extra.pdf` is the only source of cover text: a leftover
top-level `extra.cover_tagline` no longer renders when
`extra.pdf.cover_tagline` is unset for the locale being built.
- `extra_css` in both `mkdocs-base.yml` files points at `theme/`.

### Removed

`extra.pdf.copyright` and is now an error. The config it generates for
MkDocs goes to a uniquely named scratch file, so a project that already
has an `mkdocs.tmp.yml` no longer has it overwritten and deleted.
`extra.pdf` is the only source of cover text: a leftover top-level
`extra.cover_tagline` no longer renders when `extra.pdf.cover_tagline`
is unset for the locale being built.
- Removed the root `VERSION` file. The git tag is the version.

## [0.1.0] - 2026-03-30
Expand Down
46 changes: 43 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ from the tag and extracts release notes from `CHANGELOG.md`.
`fetch-theme.sh` uses `gh release download`, so the release must
exist for consumers to install a version.

## Versioning

Releases are tagged `MAJOR.MINOR.PATCH` with no `v` prefix, matching the
tag pattern (`[0-9]+.[0-9]+.[0-9]+`) that `release.yml` triggers on.
Because a single commit here controls every consumer's build and a tag
becomes a version-bump pull request in each consumer, the number states
how a release can affect a consumer's build:

- **MAJOR** — removing a markdown extension or theme feature from
`mkdocs-base.yml`, renaming or relocating an installed path, or any
other change that can break a consumer's build. Consumers must review
these against their own documents.
- **MINOR** — adding a markdown extension, theme feature, or asset.
- **PATCH** — CSS and PDF adjustments that cannot break a build.

Templates are **not** versioned separately: one repository version covers
all of them, and the vendored diff in a consumer's bump pull request
shows whether that consumer is actually affected.

A release is only meaningful when the installed surface actually changed.
The release workflow rejects a tag whose release surface — everything
`fetch-theme.sh` installs, plus `fetch-theme.sh` itself — is byte-identical
to the previous tag's, because such a tag has nothing for consumers to
fetch and would only produce an empty-diff bump pull request.

## For Consuming Projects

### Initial Setup
Expand Down Expand Up @@ -167,6 +192,20 @@ staged first in a uniquely named directory created beside it, which is
removed when the run ends, so an interrupted install leaves neither a
half-written `docs/theme/` nor anything else you keep under `docs/`.

**Commit the installed `docs/theme/` tree.** It is vendored, not
git-ignored: committing it makes the theme part of your repository, so
the site builds reproducibly for anyone who checks the project out and a
version bump lands as a reviewable diff.

The installer writes `docs/theme/.meta`, which records a `digest` of the
installed files. Verify the committed tree against that digest by
re-running the installer: a run whose `.meta` agrees with
`docs/theme.toml` and whose digest still matches the files on disk exits
without changing anything, while any edit, addition, or deletion under
`docs/theme/` is detected and reinstalled. Running `./scripts/fetch-theme.sh`
in CI is a convenient way to catch a vendored tree that has drifted from
its `.meta`.

### Wiring mkdocs.yml

Inherit the installed base config rather than copying settings out of
Expand Down Expand Up @@ -334,9 +373,10 @@ the path of the generated config is then reported on stderr.

## GitHub Pages

On merge to `main`, the CI workflow builds all sample sites and
deploys them to GitHub Pages. The landing page links to each
template sample.
On merge to `main`, the Docs workflow builds all sample sites and
deploys them to GitHub Pages. The landing page links to each template
sample and is stamped with the commit it was built from, since the
deployed site shows unreleased state that no consumer can yet install.

## License

Expand Down
17 changes: 17 additions & 0 deletions samples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@
text-decoration: none;
}
.card .links a:hover { text-decoration: underline; }
footer.provenance {
margin-top: 48px;
color: #9ca3af;
font-size: 0.8rem;
text-align: center;
}
footer.provenance a {
color: #6b7280;
text-decoration: none;
}
footer.provenance a:hover { text-decoration: underline; }
</style>
</head>
<body>
Expand All @@ -96,5 +107,11 @@ <h2><a href="manual/">Manual</a></h2>
</div>
</div>
</div>
<!-- The Docs workflow deploys this site on every push to main, so it
shows unreleased state no consumer can yet install. The deploy
stamps the revision below so an evaluator knows what they see. -->
<footer class="provenance">
<span id="build-revision">Preview build from the local working tree</span>
</footer>
</body>
</html>
69 changes: 69 additions & 0 deletions scripts/check-release-surface.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Guard against cutting a release that consumers cannot fetch anything from.
#
# The "release surface" is exactly what fetch-theme.sh installs into a
# consumer, plus fetch-theme.sh itself, which a consumer copies. A tag
# whose surface is byte-identical to the previous tag's produces an
# empty-diff version-bump pull request in every consumer, so this guard
# rejects it. samples/, .github/, README.md, CHANGELOG.md, and lint
# configuration are repo-internal and are not part of the surface.
#
# Usage:
# ./scripts/check-release-surface.sh <new-tag>
#
# The previous tag is resolved by MAJOR.MINOR.PATCH ordering over the
# repo's release tags (the release.yml pattern [0-9]+.[0-9]+.[0-9]+).
# When no predecessor exists -- the first tag ever, or a tag that sorts
# below every existing one -- the guard passes without comparing.
#
# Requires the repository history and tags to be present: run after an
# actions/checkout with fetch-depth: 0 and fetch-tags: true, otherwise
# the previous tag cannot resolve or be read.
set -euo pipefail

NEW_TAG="${1:?usage: check-release-surface.sh <new-tag>}"

# Paths fetch-theme.sh installs into a consumer, plus fetch-theme.sh
# itself. templates/ is compared whole so all four templates are covered
# by byte-diff, even the ones no sample exercises.
SURFACE_PATHS=(
templates
shared/styles/base.css
shared/fonts
shared/brand.svg
scripts/build-docs-pdf.sh
scripts/fetch-theme.sh
)

# All release tags in ascending MAJOR.MINOR.PATCH order.
tags="$(git tag --list | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V || true)"

# The predecessor is the tag immediately before NEW_TAG in that order.
# grep -B1 prints the line before the match together with the match; the
# first line of that is the predecessor. If NEW_TAG is the first (or the
# only) release tag, that first line is NEW_TAG itself -- no predecessor.
prev_tag="$(printf '%s\n' "$tags" | grep -B1 -Fx "$NEW_TAG" | head -n1 || true)"

if [ -z "$prev_tag" ] || [ "$prev_tag" = "$NEW_TAG" ]; then
echo "No predecessor tag for $NEW_TAG; skipping release-surface comparison."
exit 0
fi

echo "Comparing release surface: $prev_tag -> $NEW_TAG"

if git diff --quiet "$prev_tag" "$NEW_TAG" -- "${SURFACE_PATHS[@]}"; then
cat >&2 <<EOF
Release surface is byte-identical between $prev_tag and $NEW_TAG.

There is nothing for consumers to fetch: fetch-theme.sh would install the
same tree it installed for $prev_tag, so cutting $NEW_TAG only produces an
empty-diff version-bump pull request in every consumer. This tag should
not have been cut.

Compared paths:
$(printf ' %s\n' "${SURFACE_PATHS[@]}")
EOF
exit 1
fi

echo "Release surface differs between $prev_tag and $NEW_TAG; proceeding."
Loading