diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..be0b08c
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,49 @@
+name: Docs
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: docs-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v7
+ with:
+ python-version: "3.13"
+ - name: Install
+ run: pip install -r docs/requirements.txt
+ - name: Build
+ run: mkdocs build --strict
+ - if: github.ref == 'refs/heads/main'
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: site
+
+ # Pull requests only build the site; main also deploys it.
+ deploy:
+ if: github.ref == 'refs/heads/main' && github.repository == 'ThinkFlowLab/system1-omni'
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.gitignore b/.gitignore
index 3857937..fd0f343 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,7 @@ target
.venv/
__pycache__/
.DS_Store
+
+# Documentation site build and environment
+/site/
+.venv-docs/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b06c929..53fa46c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,3 +49,22 @@ A coding agent can help review the diff and identify issues, but contributors
remain responsible for understanding the changes and verifying the results.
Self-review helps maintainers focus on design and correctness; it does not
replace maintainer review.
+
+## Documentation site
+
+The site at is built with MkDocs
+from the README, this guide, the frontend README and the Markdown files under
+`recipe/` and `docs/`. `mkdocs.yml` sets the navigation and, in `exclude_docs`,
+the published files. Pages keep their repository paths, so write links as
+relative paths that work on GitHub; links to files that are not published go to
+GitHub. Put images in `docs/assets/`.
+
+To run the check from the Docs workflow and preview the site, with Python 3.10 or
+later:
+
+```sh
+python3 -m venv .venv-docs
+.venv-docs/bin/pip install -r docs/requirements.txt
+.venv-docs/bin/mkdocs build --strict
+.venv-docs/bin/mkdocs serve # http://127.0.0.1:8001/system1-omni/
+```
diff --git a/README.md b/README.md
index 76944aa..3d00ab3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# System1-Omni
+Documentation:
+
A community-maintained inference engine for prefill-only System1-Omni models, designed around a Rust frontend, model-owned execution, and high-performance CUDA and Metal backends.
The Rust frontend forwards requests to a separately running model worker. In-repository model engines and GPU backends are not implemented yet.
diff --git a/docs/hooks.py b/docs/hooks.py
new file mode 100644
index 0000000..7f86b2d
--- /dev/null
+++ b/docs/hooks.py
@@ -0,0 +1,58 @@
+"""MkDocs hooks that build the site from the repository root.
+
+MkDocs rejects `docs_dir: .`, so the root is set here, after the config is
+checked, and `exclude_docs` in mkdocs.yml picks the published files. Pages keep
+their repository paths, so relative links work as they do on GitHub. Inline
+links to files that are not published, or to directories without a README, go
+to GitHub.
+"""
+
+import logging
+import posixpath
+import re
+from pathlib import Path
+
+log = logging.getLogger("mkdocs.hooks")
+ROOT = Path(__file__).resolve().parent.parent
+IMAGES = (".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp")
+# The target of an inline link that is a relative path: ](path#anchor)
+LINK = re.compile(r"\]\((?![a-z][a-z0-9+.-]*:|#|/)([^)\s#]+)(#[^)\s]*)?\)")
+
+
+def on_config(config):
+ config.docs_dir = str(ROOT)
+ return config
+
+
+def on_serve(server, config, builder):
+ # Watch the published sources only, not target/ or virtual environments.
+ server.unwatch(config.docs_dir)
+ for name in ("README.md", "CONTRIBUTING.md", "docs", "recipe", "src"):
+ server.watch(str(ROOT / name))
+ return server
+
+
+def on_page_markdown(markdown, page, config, files):
+ base = posixpath.dirname(page.file.src_uri)
+ github = config.repo_url.rstrip("/")
+
+ def published(path):
+ file = files.get_file_from_path(path)
+ return file is not None and file.inclusion.is_included()
+
+ def fix(m):
+ target, anchor = m[1], m[2] or ""
+ path = posixpath.normpath(posixpath.join(base, target))
+ if path.split("/")[0] == ".." or not (ROOT / path).exists() or published(path):
+ return m[0] # a page, or a missing file for MkDocs to report
+ if (ROOT / path).is_dir():
+ if published(posixpath.join(path, "README.md")):
+ return f"]({posixpath.join(target, 'README.md')}{anchor})"
+ return f"]({github}/tree/main/{path}{anchor})"
+ if path.lower().endswith(IMAGES):
+ # A GitHub page is not an image; the image has to be published.
+ log.warning("%s: %s is not published, see exclude_docs", page.file.src_uri, target)
+ return m[0]
+ return f"]({github}/blob/main/{path}{anchor})"
+
+ return LINK.sub(fix, markdown)
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..4d732ea
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,3 @@
+mkdocs==1.6.1
+mkdocs-material==9.7.7
+pymdown-extensions==12.1
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..2d63d2e
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,53 @@
+site_name: System1-Omni
+site_url: https://thinkflowlab.github.io/system1-omni/
+repo_url: https://github.com/ThinkFlowLab/system1-omni
+edit_uri: edit/main/
+# The recipes run their workers on port 8000.
+dev_addr: 127.0.0.1:8001
+
+theme:
+ name: material
+ features:
+ - content.action.edit
+ - content.code.copy
+ - navigation.indexes
+ - navigation.sections
+ - search.highlight
+
+# docs/hooks.py builds the site from the repository root, so each page keeps its
+# repository path and relative links work here as they do on GitHub.
+hooks:
+ - docs/hooks.py
+# The published files; assets/ is the theme's. Nothing in hidden directories.
+exclude_docs: |
+ *
+ !/README.md
+ !/CONTRIBUTING.md
+ !/docs/**/*.md
+ !/docs/assets/**
+ !/recipe/**/*.md
+ !/src/frontend/README.md
+ !/assets/**
+ **/.*/**
+
+validation:
+ absolute_links: warn
+ unrecognized_links: warn
+ anchors: warn
+
+markdown_extensions:
+ - toc:
+ permalink: true
+ # Heading anchors as GitHub makes them.
+ slugify: !!python/object/apply:pymdownx.slugs.slugify
+ kwds:
+ case: lower
+ - pymdownx.superfences
+
+nav:
+ - Overview: README.md
+ - Frontend: src/frontend/README.md
+ - Recipes:
+ - recipe/README.md
+ - Laya text worker: recipe/laya/README.md
+ - Contributing: CONTRIBUTING.md