Flatten MkDocs Structure + --module-is-source Support (#4)

# Merge Request: Flatten MkDocs Structure + `--module-is-source` Support

## Summary

This MR introduces structural improvements to the MkDocs generation pipeline to:

1. Ensure a root `docs/index.md` always exists
2. Flatten documentation structure (remove `docs/<module>/` nesting by default)
3. Add support for `--module-is-source` to treat the module as the documentation root
4. Align navigation (`docforge.nav.yml`) with the new flat layout
5. Regenerate MCP artifacts to reflect updated signatures and docstrings

This resolves static hosting issues (e.g., Nginx 403 due to missing `site/index.html`) and makes each generated MkDocs site deployable as a standalone static website.

---

## Motivation

Previously, documentation was generated under:

```
docs/<module>/...
```

Which resulted in:

```
site/<module>/index.html
```

When deployed at `/libs/<project>/`, this caused:

* Missing `site/index.html`
* Nginx returning 403 for root access
* Inconsistent static hosting behavior

This MR corrects the architecture so each MkDocs build is a valid static site with a root entry point.

---

## Key Changes

### 1️⃣ Flattened Docs Structure

**Before**

```
docs/docforge/index.md
```

**After**

```
docs/index.md
```

All documentation paths were updated accordingly:

* `docs/docforge/cli/...` → `docs/cli/...`
* `docs/docforge/models/...` → `docs/models/...`
* `docs/docforge/renderers/...` → `docs/renderers/...`

Navigation updated to match the flat layout.

---

### 2️⃣ Root Index Enforcement

`MkDocsRenderer` now guarantees:

* `docs/index.md` is always created
* Parent `index.md` files are auto-generated if missing
* Parent indexes link to child modules (idempotent behavior)

This ensures:

```
site/index.html
```

Always exists after `mkdocs build`.

---

### 3️⃣ New CLI Flag: `--module-is-source`

Added option:

```
--module-is-source
```

Behavior:

* Treats the provided module as the documentation root
* Removes the top-level module folder from generated paths
* Prevents redundant nesting when the module corresponds to the source root

Updated components:

* `cli.commands.build`
* `mkdocs_utils.generate_sources`
* `MkDocsRenderer.generate_sources`
* Stub files (`.pyi`)
* MCP JSON artifacts

---

### 4️⃣ Navigation Spec Update

`docforge.nav.yml` updated:

**Before**

```yaml
home: docforge/index.md
```

**After**

```yaml
home: index.md
```

All group paths adjusted to remove `docforge/` prefix.

---

### 5️⃣ MkDocs Config Update

`mkdocs.yml` updated to:

* Move `site_name` below theme/plugins
* Use flat navigation paths
* Point Home to `index.md`

---

### 6️⃣ MCP Artifact Regeneration

Updated:

* Function signatures (new parameter)
* Docstrings (reflect `module_is_source`)
* Renderer metadata
* Line numbers

Ensures MCP output matches updated API.

---

## Architectural Outcome

Each project now produces a **valid standalone static site**:

```
site/
  index.html
  assets/
  search/
```

Safe for deployment under:

```
/libs/<project>/
```

No Nginx rewrites required.
No directory-index issues.
No nested-site ambiguity.

---

## Backward Compatibility

* Existing CLI usage remains valid
* `--module-is-source` is optional
* Navigation spec format unchanged (only paths adjusted)

---

## Deployment Impact

After merge:

* Each library can be deployed independently
* Sites can be merged under a shared root without internal conflicts
* Static hosting is predictable and production-safe

---

## Testing

* Verified MkDocs build produces `site/index.html`
* Verified navigation renders correctly
* Verified parent index generation is idempotent
* Regenerated MCP docs and validated schema consistency

---

## Result

The documentation compiler now:

* Produces structurally correct static sites
* Supports flat and source-root modes
* Eliminates 403 root issues
* Scales cleanly across multiple repositories

This aligns doc-forge with proper static-site architectural invariants.

Reviewed-on: #4
Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
This commit is contained in:
2026-02-21 16:16:36 +00:00
committed by aetos
parent 56fb39de08
commit 22fceef020
38 changed files with 306 additions and 218 deletions

View File

@@ -1,11 +1,16 @@
"""
This module implements the MkDocsRenderer, which generates Markdown source files
compatible with the MkDocs 'material' theme and 'mkdocstrings' extension.
MkDocsRenderer
Generates Markdown source files compatible with MkDocs Material
and mkdocstrings, ensuring:
- Root index.md always exists
- Parent package indexes are created automatically
- Child modules are linked in parent index files
"""
from pathlib import Path
from docforge.models import Project
from docforge.models import Project, Module
class MkDocsRenderer:
@@ -16,7 +21,15 @@ class MkDocsRenderer:
name = "mkdocs"
def generate_sources(self, project: Project, out_dir: Path) -> None:
# -------------------------
# Public API
# -------------------------
def generate_sources(
self,
project: Project,
out_dir: Path,
module_is_source: bool | None = None,
) -> None:
"""
Produce a set of Markdown files in the output directory based on the
provided Project models.
@@ -24,7 +37,11 @@ class MkDocsRenderer:
Args:
project: The project models to render.
out_dir: Target directory for documentation files.
module_is_source: Module is the source folder and to be treated as the root folder.
"""
out_dir.mkdir(parents=True, exist_ok=True)
self._ensure_root_index(project, out_dir)
modules = list(project.get_all_modules())
paths = {m.path for m in modules}
@@ -35,12 +52,23 @@ class MkDocsRenderer:
}
for module in modules:
self._write_module(module, packages, out_dir)
self._write_module(
module,
packages,
out_dir,
module_is_source,
)
# -------------------------
# Internal helpers
# -------------------------
def _write_module(self, module, packages: set[str], out_dir: Path) -> None:
def _write_module(
self,
module: Module,
packages: set[str],
out_dir: Path,
module_is_source: bool | None = None,
) -> None:
"""
Write a single module's documentation file. Packages are written as
'index.md' inside their respective directories.
@@ -49,30 +77,36 @@ class MkDocsRenderer:
module: The module to write.
packages: A set of module paths that are identified as packages.
out_dir: The base output directory.
module_is_source: Module is the source folder and to be treated as the root folder.
"""
parts = module.path.split(".")
if module_is_source:
module_name, parts = parts[0], parts[1:]
else:
module_name, parts = parts[0], parts
if module.path in packages:
# package → index.md
# Package → directory/index.md
dir_path = out_dir.joinpath(*parts)
dir_path.mkdir(parents=True, exist_ok=True)
md_path = dir_path / "index.md"
title = parts[-1].replace("_", " ").title()
link_target = f"{parts[-1]}/" if parts else None
else:
# leaf module → <name>.md
# Leaf module → parent_dir/<name>.md
dir_path = out_dir.joinpath(*parts[:-1])
dir_path.mkdir(parents=True, exist_ok=True)
md_path = dir_path / f"{parts[-1]}.md"
title = parts[-1].replace("_", " ").title()
link_target = f"{parts[-1]}.md" if parts else None
title = parts[-1].replace("_", " ").title() if parts else module_name
content = self._render_markdown(title, module.path)
if md_path.exists() and md_path.read_text(encoding="utf-8") == content:
return
if not md_path.exists() or md_path.read_text(encoding="utf-8") != content:
md_path.write_text(content, encoding="utf-8")
md_path.write_text(content, encoding="utf-8")
if not module_is_source:
self._ensure_parent_index(parts, out_dir, link_target, title)
def _render_markdown(self, title: str, module_path: str) -> str:
"""
@@ -89,3 +123,46 @@ class MkDocsRenderer:
f"# {title}\n\n"
f"::: {module_path}\n"
)
def _ensure_root_index(
self,
project: Project,
out_dir: Path
) -> None:
root_index = out_dir / "index.md"
if not root_index.exists():
root_index.write_text(
f"# {project.name}\n\n"
"## Modules\n\n",
encoding="utf-8",
)
def _ensure_parent_index(
self,
parts: list[str],
out_dir: Path,
link_target: str,
title: str,
) -> None:
if len(parts) == 1:
parent_index = out_dir / "index.md"
link = f"- [{title}]({link_target})\n"
else:
parent_dir = out_dir.joinpath(*parts[:-1])
parent_dir.mkdir(parents=True, exist_ok=True)
parent_index = parent_dir / "index.md"
link = f"- [{title}]({link_target})\n"
if not parent_index.exists():
parent_title = parts[-2].replace("_", " ").title()
parent_index.write_text(
f"# {parent_title}\n\n",
encoding="utf-8",
)
content = parent_index.read_text(encoding="utf-8")
if link not in content:
parent_index.write_text(content + link, encoding="utf-8")