Skip to content

Commit dfa737f

Browse files
docs: configure mkdocs documentation site and GitHub Pages deployment (#508)
1 parent d60ef64 commit dfa737f

File tree

7 files changed

+554
-0
lines changed

7 files changed

+554
-0
lines changed

.claude/mkdocs_hooks.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
"""
2+
MkDocs hooks for dynamic README.md discovery and navigation generation.
3+
This hook discovers all README.md files in the htk directory structure,
4+
creates symbolic links, and dynamically generates the navigation config.
5+
"""
6+
7+
from pathlib import Path
8+
9+
10+
def get_title_from_readme(path: Path) -> str:
11+
"""Extract the first heading from a README file to use as title."""
12+
try:
13+
with open(path, 'r', encoding='utf-8') as f:
14+
for line in f:
15+
line = line.strip()
16+
if line.startswith('# '):
17+
return line[2:].strip()
18+
except Exception:
19+
pass
20+
return None
21+
22+
23+
def format_name(dirname: str) -> str:
24+
"""Convert directory name to readable format."""
25+
# Convert snake_case to Title Case
26+
words = dirname.replace('_', ' ').split()
27+
return ' '.join(word.capitalize() for word in words)
28+
29+
30+
def generate_nav_config(htk_base: Path) -> list:
31+
"""
32+
Generate complete mkdocs navigation structure from filesystem.
33+
Scans for README.md files and builds nested navigation dynamically.
34+
"""
35+
nav_config = []
36+
37+
# Home
38+
nav_config.append({'Home': 'index.md'})
39+
40+
# Top-level modules (direct children of htk/)
41+
top_level_modules = [
42+
'admin', 'admintools', 'api', 'cache', 'constants',
43+
'decorators', 'extensions', 'forms', 'middleware', 'models',
44+
'scripts', 'test_scaffold', 'templatetags', 'utils', 'validators'
45+
]
46+
47+
for module in top_level_modules:
48+
module_path = htk_base / module
49+
if (module_path / 'README.md').exists():
50+
title = get_title_from_readme(module_path / 'README.md')
51+
if not title:
52+
title = format_name(module)
53+
nav_config.append({title: f'{module}.md'})
54+
55+
# Django Apps with submenu
56+
apps_path = htk_base / 'apps'
57+
if apps_path.exists():
58+
apps_nav = [{'Overview': 'apps.md'}]
59+
apps = sorted([
60+
d for d in apps_path.iterdir()
61+
if d.is_dir() and not d.name.startswith(('_', '__'))
62+
and (d / 'README.md').exists()
63+
])
64+
65+
for app_dir in apps:
66+
title = get_title_from_readme(app_dir / 'README.md')
67+
if not title:
68+
title = format_name(app_dir.name)
69+
apps_nav.append({title: f'apps/{app_dir.name}.md'})
70+
71+
nav_config.append({'Django Apps': apps_nav})
72+
73+
# Libraries with submenu
74+
lib_path = htk_base / 'lib'
75+
if lib_path.exists():
76+
libs_nav = [{'Overview': 'lib.md'}]
77+
libs = sorted([
78+
d for d in lib_path.iterdir()
79+
if d.is_dir() and not d.name.startswith(('_', '__'))
80+
and (d / 'README.md').exists()
81+
])
82+
83+
for lib_dir in libs:
84+
title = get_title_from_readme(lib_dir / 'README.md')
85+
if not title:
86+
title = format_name(lib_dir.name)
87+
libs_nav.append({title: f'lib/{lib_dir.name}.md'})
88+
89+
nav_config.append({'Libraries': libs_nav})
90+
91+
return nav_config
92+
93+
94+
def create_symlinks(docs_dir: Path, htk_base: Path):
95+
"""
96+
Create necessary symbolic links from docs directory to actual README.md files.
97+
This allows mkdocs to find all READMEs dynamically.
98+
"""
99+
# Ensure docs/apps and docs/lib directories exist
100+
(docs_dir / 'apps').mkdir(exist_ok=True)
101+
(docs_dir / 'lib').mkdir(exist_ok=True)
102+
103+
# Map of top-level directory names to documentation file names
104+
# This creates symlinks like: admin.md -> ../admin/README.md
105+
top_level_dirs = [
106+
'admin', 'admintools', 'api', 'cache', 'constants', 'decorators',
107+
'extensions', 'forms', 'middleware', 'models', 'utils', 'validators',
108+
'test_scaffold', 'scripts', 'templatetags'
109+
]
110+
111+
# Create symlinks for top-level modules
112+
for module_name in top_level_dirs:
113+
module_dir = htk_base / module_name
114+
if module_dir.exists() and module_dir.is_dir():
115+
readme = module_dir / 'README.md'
116+
if readme.exists():
117+
symlink = docs_dir / f'{module_name}.md'
118+
try:
119+
if symlink.exists() or symlink.is_symlink():
120+
symlink.unlink()
121+
symlink.symlink_to(readme)
122+
except Exception:
123+
pass
124+
125+
# Create symlink for index.md from main README.md
126+
main_readme = htk_base / 'README.md'
127+
if main_readme.exists():
128+
index_symlink = docs_dir / 'index.md'
129+
try:
130+
if index_symlink.exists() or index_symlink.is_symlink():
131+
index_symlink.unlink()
132+
index_symlink.symlink_to(main_readme)
133+
except Exception:
134+
pass
135+
136+
# Create symlinks for apps.md and lib.md overview files
137+
apps_readme = htk_base / 'apps' / 'README.md'
138+
if apps_readme.exists():
139+
apps_symlink = docs_dir / 'apps.md'
140+
try:
141+
if apps_symlink.exists() or apps_symlink.is_symlink():
142+
apps_symlink.unlink()
143+
apps_symlink.symlink_to(apps_readme)
144+
except Exception:
145+
pass
146+
147+
lib_readme = htk_base / 'lib' / 'README.md'
148+
if lib_readme.exists():
149+
lib_symlink = docs_dir / 'lib.md'
150+
try:
151+
if lib_symlink.exists() or lib_symlink.is_symlink():
152+
lib_symlink.unlink()
153+
lib_symlink.symlink_to(lib_readme)
154+
except Exception:
155+
pass
156+
157+
# Clean up old directory-level README.md symlinks that shouldn't exist
158+
# (We use top-level .md symlinks instead)
159+
old_dirs = ['api', 'cache', 'decorators', 'forms', 'middleware', 'models', 'utils', 'validators']
160+
for dir_name in old_dirs:
161+
old_readme = docs_dir / dir_name / 'README.md'
162+
if old_readme.exists() or old_readme.is_symlink():
163+
try:
164+
old_readme.unlink()
165+
except Exception:
166+
pass
167+
168+
# Create symlinks for apps
169+
apps_dir = htk_base / 'apps'
170+
if apps_dir.exists():
171+
# Create symlinks for individual apps
172+
for app_path in sorted(apps_dir.iterdir()):
173+
if app_path.is_dir() and not app_path.name.startswith(('_', '__', 'README')):
174+
readme = app_path / 'README.md'
175+
if readme.exists():
176+
symlink = docs_dir / 'apps' / f'{app_path.name}.md'
177+
try:
178+
if symlink.exists() or symlink.is_symlink():
179+
symlink.unlink()
180+
symlink.symlink_to(readme)
181+
except Exception:
182+
pass
183+
184+
# Create symlinks for libraries
185+
lib_dir = htk_base / 'lib'
186+
if lib_dir.exists():
187+
for lib_path in sorted(lib_dir.iterdir()):
188+
if lib_path.is_dir() and not lib_path.name.startswith(('_', '__', 'README')):
189+
readme = lib_path / 'README.md'
190+
if readme.exists():
191+
symlink = docs_dir / 'lib' / f'{lib_path.name}.md'
192+
try:
193+
if symlink.exists() or symlink.is_symlink():
194+
symlink.unlink()
195+
symlink.symlink_to(readme)
196+
except Exception:
197+
pass
198+
199+
200+
def on_pre_build(config, **kwargs):
201+
"""
202+
Pre-build hook to create symlinks and dynamically generate navigation.
203+
This ensures mkdocs has access to all README.md files and the nav is always in sync.
204+
"""
205+
docs_dir = Path(config['docs_dir'])
206+
htk_base = docs_dir.parent
207+
208+
# Create all necessary symlinks
209+
create_symlinks(docs_dir, htk_base)
210+
211+
# Generate and set dynamic navigation config
212+
config['nav'] = generate_nav_config(htk_base)

.github/workflows/deploy-docs.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Build and Deploy Documentation
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- main
8+
paths:
9+
- 'README.md'
10+
- '*/README.md'
11+
- 'mkdocs.yml'
12+
- 'docs/**'
13+
- '.github/workflows/deploy-docs.yml'
14+
15+
jobs:
16+
build-and-deploy:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v4
24+
with:
25+
python-version: '3.11'
26+
27+
- name: Install dependencies
28+
run: |
29+
pip install mkdocs mkdocs-material mkdocs-include-markdown-plugin
30+
31+
- name: Build documentation
32+
run: |
33+
mkdocs build
34+
35+
- name: Deploy to GitHub Pages
36+
uses: peaceiris/actions-gh-pages@v3
37+
with:
38+
github_token: ${{ secrets.GITHUB_TOKEN }}
39+
publish_dir: ./site

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
*.pyc
2+
3+
# MkDocs generated files
4+
# The docs/ folder is generated at build time by the hook
5+
docs/
6+
# The site/ folder is the HTML build output
7+
site/

docs/static/css/mkdocs.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* Custom color scheme */
2+
:root {
3+
--md-primary-fg-color: #0000e6;
4+
--md-primary-fg-color-light: #1a1aff;
5+
--md-primary-fg-color-dark: #0000b3;
6+
}
7+
8+
/* Disable bounce/rubber band effect on entire page while allowing scroll */
9+
html,
10+
body {
11+
overscroll-behavior: none;
12+
}
13+
14+
/* Disable pull-to-refresh and overscroll bounce on mobile */
15+
* {
16+
overscroll-behavior: none;
17+
}
18+
19+
/* Disable footer bounce/animation on scroll */
20+
.md-footer {
21+
position: relative !important;
22+
animation: none !important;
23+
}
24+
25+
/* Ensure footer stays fixed and doesn't bounce */
26+
.md-footer__inner {
27+
animation: none !important;
28+
}
29+
30+
/* Remove any transition animations on footer */
31+
.md-footer,
32+
.md-footer__inner {
33+
transition: none !important;
34+
}
35+
36+
/* Disable header bounce */
37+
.md-header {
38+
animation: none !important;
39+
transition: none !important;
40+
}
41+
42+
/* Hide offline plugin messages and skip links that appear below footer */
43+
.md-skip,
44+
.md-announce,
45+
[data-md-component="announce"] {
46+
display: none !important;
47+
}
48+
49+
/* Ensure page doesn't have overflow issues */
50+
html {
51+
overflow-x: hidden;
52+
}
53+
54+
/* Ensure footer is the last visible element */
55+
body {
56+
display: flex;
57+
flex-direction: column;
58+
min-height: 100vh;
59+
overflow-x: hidden;
60+
}
61+
62+
.md-container {
63+
display: flex;
64+
flex-direction: column;
65+
flex-grow: 1;
66+
}

0 commit comments

Comments
 (0)