diff --git a/TODO.md b/TODO.md
index b3eca77..92d80f5 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,14 +2,6 @@
Open work and open decisions for this repository.
-## Work
-
-- **MkDocs backend for the generator:** `src/scene_repo_template/skeleton/mkdocs/`, from the prototype on `cy/scene-docs-mkdocs` (worktree `../CodeAndDocsTemplate-mkdocs`, `api-docs/` uncommitted). It writes `docs/`, with its own docs extra (`docs_dependencies`), docs workflow and SCENE stylesheet from `scene-palette.json`; add it to `BACKENDS` in `cli.py` and to the end-to-end docs test. The logo and favicon are in `skeleton/jupyter-book/docs/images/` because `footer.md` links them relatively; move them to `common/` only if both backends can reach them there.
-- **CI on this repository:** its workflows skip private repositories, like those of generated projects. If the new repository is private, delete the `if:` lines in `.github/workflows/` here; the skeleton's keep them.
-- **Review and test pass:** `pytest` on Windows and Linux with `SCENE_TEMPLATE_E2E=1`, `pre-commit run --all-files --hook-stage manual`, a strict book build with link checking, `actionlint` and `zizmor` on `.github/` and the skeleton's `dot-github/`, the interactive prompts by hand, and every page checked in light and dark mode.
-
## Decisions
-- **New repository:** this work moves to a new SCENE repository; its name and visibility are open. After the move, update the repository links in `pyproject.toml` (`urls`), `docs/myst.yml` (`github`) and the `uvx --from` command in `docs/guides/using-the-template.md`. `uvx --from git+https://…` works without credentials only on a public repository. Do not mark it as a GitHub template repository: projects come from the generator.
-- **Footer funder line:** "Simons Foundation" now; SCENE's own wording is "supported by a Simons Foundation International grant administered by the Simons Foundation".
-- **MLAI example headers:** authors are given as initials (C. Benquet, T. Sainsbury); the permission line has no date.
+- **New repository:** this work moves to a new SCENE repository; its name and visibility are open. After the move, update the repository links in `pyproject.toml` (`urls`), `docs/myst.yml` (`github`) and `docs/guides/using-the-template.md` (uv, pip and conda tabs). Installing from `git+https://…` works without credentials only on a public repository. Do not mark it as a GitHub template repository: projects come from the generator.
diff --git a/docs/guides/development-guide.md b/docs/guides/development-guide.md
index 0727eda..ac57d4f 100644
--- a/docs/guides/development-guide.md
+++ b/docs/guides/development-guide.md
@@ -1,4 +1,4 @@
-# Development Guide
+# Collaborating on code
Each project sets its own rules, such as who reviews what and how decisions are made.
The guidelines below are a suggested common starting point.
diff --git a/docs/guides/using-the-template.md b/docs/guides/using-the-template.md
index a9242e2..fd30061 100644
--- a/docs/guides/using-the-template.md
+++ b/docs/guides/using-the-template.md
@@ -48,6 +48,7 @@ You will need to enter:
- A one-line description
- The author
- The GitHub URL
+- The documentation: Jupyter Book (guides and notebooks) or MkDocs (API reference from docstrings and notebooks)
This will show a summary, and wait for confirmation.
@@ -60,9 +61,20 @@ The project then contains:
- `pyproject.toml`, the package folder and a first test example in `tests/`;
- `.pre-commit-config.yaml` and three GitHub Actions workflows: lint, tests and docs;
-- `docs/`, a Jupyter Book with SCENE's logo, colours and footer;
+- `docs/`, a Jupyter Book, or an MkDocs site with `mkdocs.yml`;
- `LICENSE` (Apache-2.0) and a short `README.md`.
+### Which documentation backend?
+
+| | Jupyter Book | MkDocs |
+| --- | --- | --- |
+| Best for | Guides, tutorials and analyses shown as notebooks | A package with an API |
+| Needed | Markdown and notebooks (`.ipynb`), shown with their saved outputs | Markdown for the documentation, plus docstrings on the code |
+| Configuration | `docs/myst.yml` | `mkdocs.yml` |
+| Preview | `jupyter book start` in `docs/` (needs [Node.js](https://nodejs.org)) | `mkdocs serve` |
+
+A project can only have one. To switch later, generate a project with the other backend and copy its `docs/`, `.github/workflows/docs.yml`, `mkdocs.yml` if any, and the `docs` extra of `pyproject.toml`.
+
## 2. Set up your environment
In the new project, with your preferred package manager:
@@ -141,8 +153,8 @@ Commit `uv.lock` with the change.
- Create the repository on GitHub at the URL you decided, then commit and push.
- CI runs as soon as you push, if the repository is public and Actions is enabled (**Settings → Actions**): see *Continuous Integration* in the [Project standards](guidelines.md).
-- To publish the docs, enable GitHub Pages once: see [Documentation with Jupyter Book](jupyter-book.md#publish).
+- To publish the docs, enable GitHub Pages once: in **Settings → Pages**, set **Source** to **GitHub Actions**. The docs workflow then publishes on every push to `main`, for either backend. More on Jupyter Book: [Documentation with Jupyter Book](jupyter-book.md#publish).
```{important}
-No docs needed? Delete `docs/` and `.github/workflows/docs.yml`.
+No docs needed? Delete `docs/`, `.github/workflows/docs.yml`, and `mkdocs.yml` if present.
```
diff --git a/docs/index.md b/docs/index.md
index 96b91f1..476fabe 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -10,7 +10,7 @@ title: SCENE Code & Data Guidelines
Shared practices for code and data in [SCENE](https://www.project-scene.org/), the Simons Collaboration on Ecological Neuroscience, and a template repository for quick project setup.
-See the [SCENE-Collaboration](https://github.com/SCENE-Collaboration) GitHub organisation for more other repositories and resources.
+See the [SCENE-Collaboration](https://github.com/SCENE-Collaboration) GitHub organisation for other repositories and resources.
::::{grid} 1 1 2 2
diff --git a/src/scene_repo_template/cli.py b/src/scene_repo_template/cli.py
index 176b283..893acf8 100644
--- a/src/scene_repo_template/cli.py
+++ b/src/scene_repo_template/cli.py
@@ -27,6 +27,20 @@
"`.github/workflows/docs.yml` publishes it to GitHub Pages once Pages is enabled with GitHub Actions as its source."
),
},
+ "mkdocs": {
+ "label": "MkDocs: API reference from docstrings, and notebooks",
+ # MkDocs 2.0 drops the plugin system mkdocstrings relies on.
+ "docs_dependencies": (
+ '"mkdocs>=1.6,<2", "mkdocs-api-autonav>=0.1", "mkdocs-autorefs>=1.2", "mkdocs-jupyter>=0.25", '
+ '"mkdocs-material>=9.5", "mkdocstrings[python]>=0.27", "pymdown-extensions<12.2; python_version < \'3.11\'"'
+ ),
+ "docs_readme": (
+ "Built from `docs/` and `mkdocs.yml` with [MkDocs](https://www.mkdocs.org) and Material, "
+ "the API reference generated from docstrings and notebooks shown as pages: "
+ 'after `pip install -e ".[docs]"` (or `uv sync --extra docs`), `mkdocs serve` serves it locally. '
+ "`.github/workflows/docs.yml` publishes it to GitHub Pages once Pages is enabled with GitHub Actions as its source."
+ ),
+ },
}
TEMPLATE_SUFFIX = ".tmpl"
diff --git a/src/scene_repo_template/skeleton/mkdocs/docs/images/favicon.png b/src/scene_repo_template/skeleton/mkdocs/docs/images/favicon.png
new file mode 100644
index 0000000..a0de517
Binary files /dev/null and b/src/scene_repo_template/skeleton/mkdocs/docs/images/favicon.png differ
diff --git a/src/scene_repo_template/skeleton/mkdocs/docs/images/scene-logo.png b/src/scene_repo_template/skeleton/mkdocs/docs/images/scene-logo.png
new file mode 100644
index 0000000..de8ba5d
Binary files /dev/null and b/src/scene_repo_template/skeleton/mkdocs/docs/images/scene-logo.png differ
diff --git a/src/scene_repo_template/skeleton/mkdocs/docs/index.md.tmpl b/src/scene_repo_template/skeleton/mkdocs/docs/index.md.tmpl
new file mode 100644
index 0000000..03ea926
--- /dev/null
+++ b/src/scene_repo_template/skeleton/mkdocs/docs/index.md.tmpl
@@ -0,0 +1,5 @@
+# $title
+
+$description
+
+The API reference is generated from the docstrings of `$package`. Add pages, Markdown files or notebooks, next to this one and list them under `nav:` in `mkdocs.yml`.
diff --git a/src/scene_repo_template/skeleton/mkdocs/docs/stylesheets/scene.css b/src/scene_repo_template/skeleton/mkdocs/docs/stylesheets/scene.css
new file mode 100644
index 0000000..33e1176
--- /dev/null
+++ b/src/scene_repo_template/skeleton/mkdocs/docs/stylesheets/scene.css
@@ -0,0 +1,54 @@
+/* SCENE theme for MkDocs Material, in the colours of the SCENE logo. */
+
+:root {
+ --md-primary-fg-color: #125069; /* petrol blue */
+ --md-primary-fg-color--light: #12638e; /* ocean blue */
+ --md-primary-fg-color--dark: #0d3a4c;
+ --md-accent-fg-color: #41cfdb; /* cyan */
+ --md-typeset-a-color: #12638e;
+}
+
+[data-md-color-scheme="slate"] {
+ --md-primary-fg-color: #1f2d36; /* ink */
+ --md-typeset-a-color: #41cfdb;
+}
+
+/* Brand stripe above the header, in the logo's colours. */
+.md-header::before {
+ content: "";
+ display: block;
+ height: 4px;
+ background: linear-gradient(90deg, #41cfdb, #31c5a1, #80c848, #edb71f, #e98724, #d76c26);
+}
+
+.md-typeset h2 {
+ padding-bottom: 0.25em;
+ border-bottom: 2px solid var(--md-accent-fg-color);
+}
+
+.md-copyright {
+ width: 100%;
+ text-align: center;
+}
+
+/* Footer line: the name with its initials in stripe order (golden yellow skipped), as in the SCENE Jupyter Book,
+ the logo, then the funder. Markup: `copyright` in mkdocs.yml. */
+.scene-logo {
+ display: inline-block;
+ width: 1.4em;
+ height: 1.4em;
+ margin: 0 0.6em;
+ vertical-align: middle;
+ /* A background image, not an
: its URL is relative to this file, so it holds on every page. */
+ background: url("../images/scene-logo.png") center / contain no-repeat;
+}
+
+.scene-footer strong {
+ color: #fff;
+}
+
+.scene-footer em b:nth-of-type(1) { color: #41cfdb; }
+.scene-footer em b:nth-of-type(2) { color: #31c5a1; }
+.scene-footer em b:nth-of-type(3) { color: #80c848; }
+.scene-footer em b:nth-of-type(4) { color: #e98724; }
+.scene-footer em b:nth-of-type(5) { color: #d76c26; }
diff --git a/src/scene_repo_template/skeleton/mkdocs/dot-github/workflows/docs.yml b/src/scene_repo_template/skeleton/mkdocs/dot-github/workflows/docs.yml
new file mode 100644
index 0000000..9a2fb4c
--- /dev/null
+++ b/src/scene_repo_template/skeleton/mkdocs/dot-github/workflows/docs.yml
@@ -0,0 +1,46 @@
+name: Docs
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ # CI runs on public repositories only: private ones draw on the org's Actions quota, which can be billed. Delete this line to opt in.
+ if: ${{ !github.event.repository.private }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
+ with:
+ python-version: "3.13"
+ - run: uv sync --extra docs
+ - run: uv run mkdocs build --strict
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
+ with:
+ path: site
+
+ deploy:
+ # Pull requests only check that the site builds.
+ if: ${{ github.event_name == 'push' && !github.event.repository.private }}
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ concurrency:
+ group: pages
+ cancel-in-progress: false
+ steps:
+ - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
+ - id: deployment
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
diff --git a/src/scene_repo_template/skeleton/mkdocs/mkdocs.yml.tmpl b/src/scene_repo_template/skeleton/mkdocs/mkdocs.yml.tmpl
new file mode 100644
index 0000000..c2daae5
--- /dev/null
+++ b/src/scene_repo_template/skeleton/mkdocs/mkdocs.yml.tmpl
@@ -0,0 +1,83 @@
+# MkDocs configuration: https://www.mkdocs.org, theme https://squidfunk.github.io/mkdocs-material/
+# Preview locally with `mkdocs serve`; build with `mkdocs build --strict`. Pages are in docs/.
+site_name: "$title"
+site_description: "$description"
+repo_url: $github_url
+
+theme:
+ name: material
+ logo: images/scene-logo.png
+ favicon: images/favicon.png
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: custom
+ accent: custom
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: custom
+ accent: custom
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+ features:
+ - content.code.copy
+ - navigation.indexes
+ - navigation.path
+ - navigation.sections
+ - navigation.top
+ - search.highlight
+ - search.suggest
+ - toc.follow
+
+extra_css:
+ - stylesheets/scene.css
+
+plugins:
+ - search
+ - autorefs
+ # One page per module of the package, under "API reference".
+ - api-autonav:
+ modules: ["$package"]
+ nav_section_title: "API reference"
+ exclude_private: true
+ - mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ options:
+ show_root_heading: true
+ show_symbol_type_heading: true
+ separate_signature: true
+ show_signature_annotations: true
+ docstring_style: google
+ line_length: 120
+ inventories:
+ - url: https://docs.python.org/3/objects.inv
+ domains: [py, std]
+ # Notebooks (.ipynb) are pages, shown with the outputs saved in them, not re-executed.
+ # Only notebooks: by default, .py files in docs/ would become pages too.
+ - mkdocs-jupyter:
+ include: ["*.ipynb"]
+
+markdown_extensions:
+ - attr_list
+ - admonition
+ - pymdownx.details
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.inlinehilite
+ - pymdownx.superfences
+
+extra:
+ generator: false
+
+# The SCENE name with coloured initials, styled in docs/stylesheets/scene.css.
+copyright: ""
+
+nav:
+ - Home: index.md
+ - API reference
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 7b2b874..7dc1941 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,5 +1,6 @@
import importlib
import importlib.metadata
+import json
import os
import shutil
import subprocess
@@ -37,27 +38,51 @@ def refused(tmp_path, *options, folder="my-project"):
return exit_info.value.code == 2
-def test_a_project_gets_the_common_files_and_its_docs_backend(tmp_path):
- expected = {
- ".github/workflows/docs.yml",
- ".github/workflows/lint.yml",
- ".github/workflows/tests.yml",
- ".gitignore",
- ".pre-commit-config.yaml",
- "LICENSE",
- "README.md",
- "docs/_static/footer.md",
- "docs/_static/scene.css",
- "docs/images/favicon.png",
- "docs/images/scene-logo.png",
- "docs/index.md",
- "docs/myst.yml",
- "my_project/__init__.py",
- "my_project/version.py",
- "pyproject.toml",
- "tests/test_version.py",
- }
- assert expected <= files_of(generate(tmp_path))
+COMMON_FILES = {
+ ".github/workflows/docs.yml",
+ ".github/workflows/lint.yml",
+ ".github/workflows/tests.yml",
+ ".gitignore",
+ ".pre-commit-config.yaml",
+ "LICENSE",
+ "README.md",
+ "docs/images/favicon.png",
+ "docs/images/scene-logo.png",
+ "docs/index.md",
+ "my_project/__init__.py",
+ "my_project/version.py",
+ "pyproject.toml",
+ "tests/test_version.py",
+}
+BACKEND_FILES = {
+ "jupyter-book": {"docs/myst.yml", "docs/_static/footer.md", "docs/_static/scene.css"},
+ "mkdocs": {"mkdocs.yml", "docs/stylesheets/scene.css"},
+}
+
+
+def test_every_backend_is_tested():
+ assert set(BACKEND_FILES) == set(cli.BACKENDS)
+
+
+@pytest.mark.parametrize("backend", BACKEND_FILES)
+def test_a_project_gets_the_common_files_and_only_its_own_docs_backend(tmp_path, backend):
+ project = files_of(generate(tmp_path, f"--docs={backend}"))
+ assert COMMON_FILES | BACKEND_FILES[backend] <= project
+ other_backends = set().union(*(files for name, files in BACKEND_FILES.items() if name != backend))
+ assert project & other_backends == set()
+
+
+def test_the_backends_ship_the_same_logo_and_favicon():
+ for image in ("scene-logo.png", "favicon.png"):
+ copies = {(cli.SKELETON / backend / "docs" / "images" / image).read_bytes() for backend in cli.BACKENDS}
+ assert len(copies) == 1, image
+
+
+def test_the_docs_extra_follows_the_backend(tmp_path):
+ pyproject = (generate(tmp_path, "--docs=mkdocs") / "pyproject.toml").read_text(encoding="utf-8")
+ assert '"mkdocstrings[python]>=0.27"' in pyproject
+ assert '"mkdocs-jupyter>=0.25"' in pyproject
+ assert "jupyter-book" not in pyproject
def test_no_skeleton_naming_is_left_in_a_project(tmp_path):
@@ -303,7 +328,21 @@ def test_a_generated_project_installs_lints_and_passes_its_tests(tmp_path):
@E2E
@pytest.mark.skipif(shutil.which("node") is None, reason="Jupyter Book needs Node.js")
-def test_a_generated_project_builds_its_docs(tmp_path):
- dest = generate(tmp_path)
+def test_a_generated_jupyter_book_builds(tmp_path):
+ dest = generate(tmp_path, "--docs=jupyter-book")
run(dest, "uv", "sync", "--extra", "docs")
run(dest / "docs", "uv", "run", "jupyter", "book", "build", "--html", "--strict")
+
+
+@E2E
+def test_a_generated_mkdocs_site_builds_with_an_api_page_per_module_and_a_notebook(tmp_path):
+ dest = generate(tmp_path, "--docs=mkdocs")
+ notebook = {"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Analysis"]}], "metadata": {}}
+ notebook |= {"nbformat": 4, "nbformat_minor": 5}
+ (dest / "docs" / "analysis.ipynb").write_text(json.dumps(notebook), encoding="utf-8")
+ mkdocs_yml = dest / "mkdocs.yml"
+ mkdocs_yml.write_text(mkdocs_yml.read_text(encoding="utf-8") + " - analysis.ipynb\n", encoding="utf-8")
+ run(dest, "uv", "sync", "--extra", "docs")
+ run(dest, "uv", "run", "mkdocs", "build", "--strict")
+ assert (dest / "site" / "reference" / "my_project" / "version" / "index.html").is_file()
+ assert (dest / "site" / "analysis" / "index.html").is_file()