Skip to content

Commit 6563744

Browse files
feat: build .md for each web page, so that {url}/quickstart.md works in the project website
1 parent c84460b commit 6563744

7 files changed

Lines changed: 233 additions & 3 deletions

File tree

pixi.lock

Lines changed: 100 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pixi.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ gitpython = ">=3.1.49"
3232
# pygithub = ">=2.8.1"
3333
python-dotenv = ">=1.2.2"
3434
pyproject-metadata = ">=0.11.0"
35-
# jupytext = ">=1.18.1"
35+
jupytext = ">=1.19.1"
3636
# jinja2 = ">=3.1.6"
3737
mystmd = ">=1.9.0"
3838
great-docs = ">=0.9"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies = [
4141
# "pygithub>=2.8.1",
4242
"python-dotenv>=1.2.2",
4343
"pyproject-metadata>=0.11.0",
44-
# "jupytext>=1.18.1",
44+
"jupytext>=1.19.1",
4545
# "jinja2>=3.1.6",
4646
"mystmd>=1.9.0",
4747
"great-docs>=0.9",

src/afterpython/builders/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from afterpython.builders.jupyter_notebook import build_jupyter_notebooks
77
from afterpython.builders.markdown import build_markdown
88
from afterpython.builders.metadata import build_metadata
9+
from afterpython.builders.url_md import build_url_md
910

1011
__all__ = (
1112
"build_content_json",
1213
"build_jupyter_notebooks",
1314
"build_markdown",
1415
"build_metadata",
16+
"build_url_md",
1517
"create_placeholder_index_md_files",
1618
"delete_placeholder_index_md_files",
1719
)

src/afterpython/builders/url_md.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Publish raw markdown alongside rendered HTML.
2+
3+
For every page the website serves at `/{type}/{slug}`, also publish the source
4+
markdown at `/{type}/{slug}.md`. Lets LLMs and AI agents fetch clean content
5+
without HTML chrome (the Mintlify / Anthropic-docs pattern).
6+
7+
Output goes into `afterpython/_website/static/{type}/{slug}.md`. SvelteKit's
8+
static adapter then serves each file at the matching URL with no SvelteKit-side
9+
code changes.
10+
11+
Flow
12+
----
13+
build_url_md() # entry point
14+
└─ for each content_type in CONTENT_TYPES:
15+
└─ for each `*.md` / `*.ipynb` under afterpython/{type}/:
16+
├─ _slugify(stem) → URL slug
17+
├─ .md → shutil.copy(source, dest) (frontmatter preserved)
18+
└─ .ipynb → jupytext.read + write(fmt="md") (cell outputs stripped)
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import shutil
24+
from pathlib import Path
25+
26+
import click
27+
import jupytext
28+
29+
import afterpython as ap
30+
from afterpython._typing import tContentType
31+
from afterpython.const import CONTENT_TYPES
32+
33+
34+
def _slugify(stem: str) -> str:
35+
"""Mirror MyST's URL slug rule so each `.md` lands at the same URL as its
36+
rendered HTML sibling.
37+
38+
`package_management.md` -> `/doc/package-management.md`
39+
`CONTRIBUTING.md` -> `/doc/contributing.md`
40+
"""
41+
return stem.lower().replace("_", "-")
42+
43+
44+
def _publish_md(source: Path, dest: Path) -> None:
45+
"""Copy a `.md` source verbatim. Frontmatter is preserved — LLMs can use
46+
it (date, authors, tags) and it's harmless if ignored.
47+
"""
48+
shutil.copy(source, dest)
49+
50+
51+
def _publish_ipynb(source: Path, dest: Path) -> None:
52+
"""Convert a notebook to clean markdown via jupytext.
53+
54+
jupytext strips cell outputs by default, producing a leaner file than
55+
`nbconvert` would. The `md` format is jupytext's default — clean enough
56+
for LLM consumption while still round-trippable if anyone needs the
57+
notebook back.
58+
"""
59+
notebook = jupytext.read(source)
60+
jupytext.write(notebook, dest, fmt="md")
61+
62+
63+
def _publish_for_type(content_type: tContentType) -> int:
64+
"""Walk one content type's source dir; publish every supported file.
65+
Returns the number of files written.
66+
"""
67+
source_dir = ap.paths.afterpython_path / content_type
68+
if not source_dir.exists():
69+
return 0
70+
71+
dest_dir = ap.paths.website_path / "static" / content_type
72+
dest_dir.mkdir(parents=True, exist_ok=True)
73+
74+
count = 0
75+
for source in sorted(source_dir.rglob("*")):
76+
if "_build" in source.parts:
77+
continue
78+
if not source.is_file():
79+
continue
80+
81+
slug = _slugify(source.stem)
82+
dest = dest_dir / f"{slug}.md"
83+
84+
if source.suffix == ".md":
85+
_publish_md(source, dest)
86+
elif source.suffix == ".ipynb":
87+
_publish_ipynb(source, dest)
88+
else:
89+
continue
90+
91+
count += 1
92+
return count
93+
94+
95+
def build_url_md() -> None:
96+
"""Publish raw markdown for every content type alongside the rendered HTML.
97+
98+
Runs as part of `postbuild()` after MyST's HTML has been copied into
99+
`_website/static/{type}/`. Each `.md` ends up as a sibling of the rendered
100+
`{slug}/index.html`, served at `/{type}/{slug}.md` by SvelteKit's static
101+
adapter.
102+
"""
103+
for content_type in CONTENT_TYPES:
104+
count = _publish_for_type(content_type)
105+
if count > 0:
106+
click.echo(f"Published {count} url-md file(s) for {content_type}/")

src/afterpython/cli/commands/build.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
build_jupyter_notebooks,
2121
build_markdown,
2222
build_metadata,
23+
build_url_md,
2324
create_placeholder_index_md_files,
2425
delete_placeholder_index_md_files,
2526
)
@@ -164,6 +165,10 @@ def _move_files(
164165
# Copy all static files from afterpython/static/ to afterpython/_website/static/
165166
_move_files(ap.paths.static_path, website_static, is_copy=True)
166167

168+
# Publish raw markdown alongside rendered HTML so each page is fetchable
169+
# at /{type}/{slug}.md for LLMs / AI agents (Mintlify-style).
170+
build_url_md()
171+
167172

168173
@click.command(
169174
add_help_option=False, # disable click's --help option so that ap build --help can work

uv.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)