Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ plugins:
Community:
- community/*.md

# Hooks
hooks:
- scripts/flatten_md.py

# Navigation
nav:
- Home: index.md
Expand Down
7 changes: 3 additions & 4 deletions overrides/partials/actions.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
{% include ".icons/" ~ icon ~ ".svg" %}
</a>
{% endif %}
{% set md_href = (page.url.rstrip('/') ~ ".md") | url %}
{% if "content.action.view" in features %}
{% set view_url = page.url | url ~ "index.md" %}
<a href="{{ view_url }}" title="View this page as Markdown" class="md-content__button md-icon">
<a href="{{ md_href }}" title="View this page as Markdown" class="md-content__button md-icon">
{% set icon = config.theme.icon.view or "material/file-eye-outline" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</a>
{% endif %}
{% set raw_url = page.url | url ~ "index.md" %}
<button id="copy-page-btn" class="md-content__button md-icon" title="Copy page as Markdown" data-url="{{ raw_url }}">
<button id="copy-page-btn" class="md-content__button md-icon" title="Copy page as Markdown" data-url="{{ md_href }}">
{% include ".icons/material/content-copy.svg" %}
</button>
{% endif %}
93 changes: 93 additions & 0 deletions scripts/flatten_md.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""MkDocs post-build hook: flatten per-page Markdown from ``page/index.md`` to
``page.md``.

Registered via the top-level ``hooks:`` list in ``mkdocs.yml``. Runs after the
``mkdocs-llmstxt`` plugin (see the ``event_priority`` on ``on_post_build``).
"""

from __future__ import annotations

import logging
import re
from pathlib import Path, PurePosixPath

import mkdocs.plugins


def flat_md_target(rel_path: str) -> str | None:
"""Map a generated Markdown file's site-relative path to its flat target.

Args:
rel_path: Path of the generated file relative to ``site_dir``, POSIX form.

Returns:
The new site-relative path (e.g. ``agents/config.md``), or ``None`` if
the file must stay in place (root ``index.md`` or a non-``index.md`` file).
"""
p = PurePosixPath(rel_path)
if p.name != "index.md":
return None
parent = p.parent
if parent in (PurePosixPath("."), PurePosixPath("")):
return None # root site/index.md stays as-is
return f"{parent.as_posix()}.md"


def rewrite_index_links(text: str, site_url: str) -> str:
"""Rewrite ``{site_url}/<path>/index.md`` links to ``{site_url}/<path>.md``.

The bare homepage link ``{site_url}/index.md`` has no intermediate path
segment and is left unchanged. Scoping to ``site_url`` avoids corrupting
unrelated URLs that merely contain the substring ``index.md``.
"""
base = site_url.rstrip("/")
pattern = re.compile(re.escape(base) + r"/([^\s)]+?)/index\.md")
return pattern.sub(lambda m: f"{base}/{m.group(1)}.md", text)


log = logging.getLogger("mkdocs.hooks.flatten_md")


@mkdocs.plugins.event_priority(-100)
def on_post_build(config, **kwargs) -> None: # noqa: ARG001
"""Rename generated ``index.md`` files to flat ``page.md`` and fix links.

Priority ``-100`` ensures this runs after the ``mkdocs-llmstxt`` plugin's
own ``on_post_build`` (lower priority runs later).
"""
site_dir = Path(config["site_dir"])
site_url = config["site_url"]

# Pass 1: rename <dir>/index.md -> <dir>.md (root index.md is left alone).
renamed = 0
for md_path in list(site_dir.rglob("index.md")):
rel = md_path.relative_to(site_dir).as_posix()
target_rel = flat_md_target(rel)
if target_rel is None:
continue
md_path.replace(site_dir / target_rel)
renamed += 1

# Pass 2: rewrite site-internal .../index.md links in all text outputs.
# Load-bearing assumption: internal .md links are absolute {site_url}/... URLs
# because mkdocs-llmstxt normalizes them via its _convert_to_absolute_link, so
# rewrite_index_links only handles absolute links; relative .../index.md links
# are not expected here.
rewritten = 0
targets = list(site_dir.rglob("*.md"))
for extra in ("llms.txt", "llms-full.txt"):
extra_path = site_dir / extra
if extra_path.exists():
targets.append(extra_path)
for path in targets:
text = path.read_text(encoding="utf8")
new_text = rewrite_index_links(text, site_url)
if new_text != text:
path.write_text(new_text, encoding="utf8")
rewritten += 1

log.info(
"flatten_md: renamed %d markdown file(s), rewrote links in %d file(s)",
renamed,
rewritten,
)
118 changes: 118 additions & 0 deletions scripts/test_flatten_md.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from flatten_md import flat_md_target, on_post_build, rewrite_index_links

SITE = "https://adk.dev"


def test_flat_md_target_regular_page():
assert flat_md_target("agents/config/index.md") == "agents/config.md"


def test_flat_md_target_section_index():
assert flat_md_target("agents/index.md") == "agents.md"


def test_flat_md_target_root_index_stays():
assert flat_md_target("index.md") is None


def test_flat_md_target_non_index_ignored():
assert flat_md_target("agents/config.md") is None


def test_rewrite_links_regular_page():
text = "[cfg](https://adk.dev/agents/config/index.md)"
assert rewrite_index_links(text, SITE) == "[cfg](https://adk.dev/agents/config.md)"


def test_rewrite_links_section_index():
text = "See https://adk.dev/agents/index.md for details"
assert rewrite_index_links(text, SITE) == "See https://adk.dev/agents.md for details"


def test_rewrite_links_homepage_unchanged():
text = "Home: https://adk.dev/index.md"
assert rewrite_index_links(text, SITE) == "Home: https://adk.dev/index.md"


def test_rewrite_links_trailing_slash_site_url():
text = "https://adk.dev/agents/config/index.md"
assert rewrite_index_links(text, "https://adk.dev/") == "https://adk.dev/agents/config.md"


def test_rewrite_links_idempotent():
once = rewrite_index_links("https://adk.dev/agents/config/index.md", SITE)
assert rewrite_index_links(once, SITE) == once


def _build_fake_site(site_root):
"""Create a realistic post-build site layout under ``site_root``.

Returns the ``config`` dict expected by ``on_post_build``.
"""
(site_root / "agents" / "config").mkdir(parents=True)
(site_root / "agents" / "config" / "index.md").write_text(
"[agents](https://adk.dev/agents/index.md)\n"
"[qs](https://adk.dev/get-started/quickstart/index.md)\n",
encoding="utf8",
)
(site_root / "agents" / "index.md").write_text(
"[cfg](https://adk.dev/agents/config/index.md)\n",
encoding="utf8",
)
(site_root / "index.md").write_text(
"Home: https://adk.dev/index.md\n",
encoding="utf8",
)
(site_root / "llms.txt").write_text(
"- [X](https://adk.dev/agents/config/index.md)\n",
encoding="utf8",
)
(site_root / "llms-full.txt").write_text(
"See https://adk.dev/agents/index.md for details\n",
encoding="utf8",
)
return {"site_dir": str(site_root), "site_url": SITE}


def test_on_post_build_renames_layout(tmp_path):
site_root = tmp_path / "site"
config = _build_fake_site(site_root)

on_post_build(config)

# Flat targets exist; original nested index.md files are gone.
assert (site_root / "agents" / "config.md").exists()
assert not (site_root / "agents" / "config" / "index.md").exists()
assert (site_root / "agents.md").exists()
assert not (site_root / "agents" / "index.md").exists()

# Root index.md is left in place (not renamed).
assert (site_root / "index.md").exists()


def test_on_post_build_rewrites_links(tmp_path):
site_root = tmp_path / "site"
config = _build_fake_site(site_root)

on_post_build(config)

config_md = (site_root / "agents" / "config.md").read_text(encoding="utf8")
assert "https://adk.dev/agents.md" in config_md
assert "https://adk.dev/get-started/quickstart.md" in config_md
assert "index.md" not in config_md

agents_md = (site_root / "agents.md").read_text(encoding="utf8")
assert "https://adk.dev/agents/config.md" in agents_md
assert "index.md" not in agents_md

llms = (site_root / "llms.txt").read_text(encoding="utf8")
assert "https://adk.dev/agents/config.md" in llms
assert "index.md" not in llms

llms_full = (site_root / "llms-full.txt").read_text(encoding="utf8")
assert "https://adk.dev/agents.md" in llms_full
assert "index.md" not in llms_full

# Bare homepage link is preserved unchanged.
root_md = (site_root / "index.md").read_text(encoding="utf8")
assert root_md == "Home: https://adk.dev/index.md\n"