Skip to content

Commit 4e82e18

Browse files
committed
feat: add demo site.
1 parent 214c095 commit 4e82e18

3 files changed

Lines changed: 139 additions & 23 deletions

File tree

.github/workflows/demo.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Demo site
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
pages: write
11+
id-token: write
12+
13+
concurrency:
14+
group: pages
15+
cancel-in-progress: false
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Install uv
24+
uses: astral-sh/setup-uv@v5
25+
with:
26+
enable-cache: true
27+
28+
- name: Set up Python
29+
run: uv python install 3.12
30+
31+
- name: Install backend dependencies
32+
run: |
33+
cd backend
34+
uv venv
35+
uv pip install -r requirements.txt
36+
37+
- name: Build demo site
38+
run: |
39+
cd backend
40+
source .venv/bin/activate
41+
PYTHONPATH=. python scripts/build_demo_site.py ../public
42+
43+
- uses: actions/upload-pages-artifact@v3
44+
with:
45+
path: ./public
46+
47+
deploy:
48+
needs: build
49+
runs-on: ubuntu-latest
50+
environment:
51+
name: github-pages
52+
url: ${{ steps.deployment.outputs.page_url }}
53+
steps:
54+
- id: deployment
55+
uses: actions/deploy-pages@v4

backend/app/routers/export.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,37 @@ async def export_page(
270270
})
271271

272272

273+
def build_site_files(pages):
274+
"""Render pages into ``[(filename, html), ...]`` for a static export.
275+
276+
Shared by the HTTP zip endpoint and the CLI demo-site builder so both
277+
paths produce byte-identical output.
278+
"""
279+
files = []
280+
page_links = []
281+
for p in pages:
282+
page = dict(p)
283+
html_content = _inline_media_srcs(md_to_simple_html(page["content_md"]))
284+
safe_title = _html.escape(page["title"])
285+
safe_slug = _html.escape(page["slug"])
286+
full_html = HTML_TEMPLATE.format(
287+
title=safe_title,
288+
slug=safe_slug,
289+
content=html_content,
290+
)
291+
files.append((f"{page['slug']}.html", full_html))
292+
# Both href and link text need escaping — slug may contain chars
293+
# that break the attribute, title may contain HTML.
294+
page_links.append(
295+
f'<li><a href="{urllib.parse.quote(page["slug"], safe="")}.html">'
296+
f'{safe_title}</a></li>'
297+
)
298+
299+
index_html = SITE_INDEX_TEMPLATE.format(page_list="\n".join(page_links))
300+
files.append(("index.html", index_html))
301+
return files
302+
303+
273304
@router.get("/site")
274305
async def export_site(
275306
format: str = Query("html"),
@@ -287,29 +318,8 @@ async def export_site(
287318

288319
buf = io.BytesIO()
289320
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
290-
# Generate page files
291-
page_links = []
292-
for p in pages:
293-
page = dict(p)
294-
html_content = _inline_media_srcs(md_to_simple_html(page["content_md"]))
295-
safe_title = _html.escape(page["title"])
296-
safe_slug = _html.escape(page["slug"])
297-
full_html = HTML_TEMPLATE.format(
298-
title=safe_title,
299-
slug=safe_slug,
300-
content=html_content,
301-
)
302-
zf.writestr(f"{page['slug']}.html", full_html)
303-
# Both href and link text need escaping — slug may contain chars
304-
# that break the attribute, title may contain HTML.
305-
page_links.append(
306-
f'<li><a href="{urllib.parse.quote(page["slug"], safe="")}.html">'
307-
f'{safe_title}</a></li>'
308-
)
309-
310-
# Generate index
311-
index_html = SITE_INDEX_TEMPLATE.format(page_list="\n".join(page_links))
312-
zf.writestr("index.html", index_html)
321+
for filename, content in build_site_files(pages):
322+
zf.writestr(filename, content)
313323

314324
buf.seek(0)
315325
return StreamingResponse(

backend/scripts/build_demo_site.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Render the seeded welcome content into a static HTML site.
2+
3+
Run from ``backend/`` with ``PYTHONPATH=.`` so ``app`` is importable:
4+
5+
PYTHONPATH=. python scripts/build_demo_site.py ../public
6+
7+
The script points JustWiki at a throwaway data directory, runs the same
8+
init + seed path the server uses on first boot, then writes every page
9+
through ``app.routers.export.build_site_files``.
10+
"""
11+
import asyncio
12+
import os
13+
import sys
14+
import tempfile
15+
from pathlib import Path
16+
17+
18+
def main():
19+
out_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "./public").resolve()
20+
21+
tmp = Path(tempfile.mkdtemp(prefix="justwiki-demo-"))
22+
os.environ["DATA_DIR"] = str(tmp)
23+
os.environ["DB_PATH"] = str(tmp / "just-wiki.db")
24+
os.environ["MEDIA_DIR"] = str(tmp / "media")
25+
26+
# Imports must happen after env vars are set so pydantic-settings reads
27+
# the throwaway paths instead of the repo's .env / defaults.
28+
from app.auth import ensure_admin_exists
29+
from app.database import close_db, get_db, init_db, seed_welcome_page
30+
from app.routers.export import build_site_files
31+
32+
async def run():
33+
await init_db()
34+
await ensure_admin_exists()
35+
db = await get_db()
36+
await seed_welcome_page(db)
37+
pages = await db.execute_fetchall(
38+
"SELECT id, slug, title, content_md FROM pages "
39+
"WHERE deleted_at IS NULL ORDER BY title"
40+
)
41+
out_dir.mkdir(parents=True, exist_ok=True)
42+
for filename, content in build_site_files(pages):
43+
(out_dir / filename).write_text(content, encoding="utf-8")
44+
await close_db()
45+
46+
asyncio.run(run())
47+
print(f"Wrote demo site to {out_dir}")
48+
49+
50+
if __name__ == "__main__":
51+
main()

0 commit comments

Comments
 (0)