diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77e49d8..ffced1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -92,12 +92,6 @@ repos: # Documentation validation - repo: local hooks: - - id: docs-check-titles - name: docs-check-titles - entry: mise run docs-check-titles - language: system - files: ^docs/.*\.md$ - pass_filenames: false - id: docs-check-filenames name: docs-check-filenames entry: mise run docs-check-filenames diff --git a/docs/changelog.d/feature-docs-title-cleanup.doc.md b/docs/changelog.d/feature-docs-title-cleanup.doc.md new file mode 100644 index 0000000..03f5564 --- /dev/null +++ b/docs/changelog.d/feature-docs-title-cleanup.doc.md @@ -0,0 +1 @@ +Remove docs-check-titles pre-commit hook, add repo links to homepage, and test duplicate frontmatter titles. diff --git a/docs/index.md b/docs/index.md index 8d556fd..a385da8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ --- -title: blumeops-documentation +title: BlumeOps aliases: [] id: index tags: [] @@ -14,11 +14,11 @@ infrastructure. ## What is BlumeOps? BlumeOps is my personal homelab infrastructure managed entirely through code. -Everything lives in a single git repository, from service configs to deployment -automation. Even the [[forgejo]] instance that hosts this repo is defined -within it, making BlumeOps fully self-hosting. It's a digital life raft I built -for myself as I went, and you can see it all from within your editor of choice. -(I recommend vim.) +Everything lives in a [single git repository](https://github.com/eblume/blumeops), from service configs to +deployment automation. Even the [[forgejo]] instance that [hosts this repo](https://forge.ops.eblu.me/eblume/blumeops) +is defined within it, making BlumeOps fully self-hosting. It's a digital life +raft I built for myself as I went, and you can see it all from within your +editor of choice. (I recommend vim.) These services run on my home [[hosts|infrastructure]], primarily an m1 mac mini named [[indri]] and a Synology NAS called [[sifaka]]. The infrastructure diff --git a/docs/reference/infrastructure/title-test-alpha.md b/docs/reference/infrastructure/title-test-alpha.md new file mode 100644 index 0000000..cc25c2a --- /dev/null +++ b/docs/reference/infrastructure/title-test-alpha.md @@ -0,0 +1,13 @@ +--- +title: Title Test Card +tags: + - infrastructure + - test +--- + +# Title Test Card (Alpha) + +This card tests that duplicate frontmatter titles don't break wiki-link resolution. +This card and [[title-test-beta]] share the same `title:` frontmatter value. + +If you can read both cards and the links work, the test passes. diff --git a/docs/reference/infrastructure/title-test-beta.md b/docs/reference/infrastructure/title-test-beta.md new file mode 100644 index 0000000..3347cf2 --- /dev/null +++ b/docs/reference/infrastructure/title-test-beta.md @@ -0,0 +1,13 @@ +--- +title: Title Test Card +tags: + - infrastructure + - test +--- + +# Title Test Card (Beta) + +This card tests that duplicate frontmatter titles don't break wiki-link resolution. +This card and [[title-test-alpha]] share the same `title:` frontmatter value. + +If you can read both cards and the links work, the test passes. diff --git a/docs/reference/reference.md b/docs/reference/reference.md index 34f0978..3d0be42 100644 --- a/docs/reference/reference.md +++ b/docs/reference/reference.md @@ -46,6 +46,7 @@ Host inventory and network configuration. - [[tailscale]] - ACLs, groups, tags - [[gandi]] - DNS hosting for `eblu.me` - [[routing|Routing]] - DNS domains, port mappings +- [[title-test-alpha]] / [[title-test-beta]] - Duplicate title test (temporary) ## Kubernetes diff --git a/docs/tutorials/ai-assistance-guide.md b/docs/tutorials/ai-assistance-guide.md index 6b9dc86..40f6a18 100644 --- a/docs/tutorials/ai-assistance-guide.md +++ b/docs/tutorials/ai-assistance-guide.md @@ -96,7 +96,6 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail | `tailnet-up` | Apply Tailscale ACL changes via Pulumi | | `docs-check-links` | Validate wiki-links in documentation (includes orphan detection) | | `docs-check-index` | Check every doc is referenced in its category index | -| `docs-check-titles` | Check for duplicate doc titles | | `docs-check-filenames` | Check for duplicate doc filenames | | `docs-review-stale` | Report docs by last-modified date, highlight stale ones | | `docs-review-tags` | Print frontmatter tag inventory across all docs | diff --git a/mise-tasks/docs-check-titles b/mise-tasks/docs-check-titles deleted file mode 100755 index 4f162fd..0000000 --- a/mise-tasks/docs-check-titles +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.12" -# dependencies = ["pyyaml>=6.0", "rich>=13.0.0"] -# /// -#MISE description="List all doc card titles and detect duplicates or invalid formats" -"""List all documentation card titles and detect duplicates or invalid formats. - -This script scans all markdown files in the docs/ directory (excluding -changelog.d/ and zk/), extracts frontmatter titles, and reports any -duplicates or invalid formats that could cause wiki-link resolution issues. - -With Quartz, wiki-links like [[title]] resolve by frontmatter title, -so titles must be: -- Unique across the documentation -- Lowercase with hyphens (no spaces or uppercase) - -Usage: mise run docs-check-titles -""" - -import re -import sys -from collections import defaultdict -from pathlib import Path - -import yaml -from rich.console import Console -from rich.table import Table - -DOCS_DIR = Path(__file__).parent.parent / "docs" - -# Valid title pattern: lowercase letters, numbers, hyphens only -VALID_TITLE_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") - - -def extract_frontmatter(file_path: Path) -> dict | None: - """Extract YAML frontmatter from a markdown file.""" - content = file_path.read_text() - if not content.startswith("---"): - return None - - end_idx = content.find("---", 3) - if end_idx == -1: - return None - - frontmatter_text = content[3:end_idx].strip() - try: - return yaml.safe_load(frontmatter_text) or {} - except yaml.YAMLError: - return None - - -def is_valid_slug(title: str) -> bool: - """Check if title is a valid slug (lowercase, hyphens, no spaces).""" - return bool(VALID_TITLE_PATTERN.match(title)) - - -def main() -> int: - console = Console() - - # Collect all titles and their source files - # Key: title, Value: list of file paths - titles: dict[str, list[str]] = defaultdict(list) - invalid_titles: list[tuple[str, str]] = [] # (title, file_path) - - # Scan all markdown files (excluding zk/ and changelog.d/) - for md_file in sorted(DOCS_DIR.rglob("*.md")): - # Skip changelog fragments and zk cards - if "changelog.d" in md_file.parts or "zk" in md_file.parts: - continue - - rel_path = str(md_file.relative_to(DOCS_DIR)) - frontmatter = extract_frontmatter(md_file) - - if not frontmatter: - continue - - title = frontmatter.get("title") - if title: - titles[title].append(rel_path) - if not is_valid_slug(title): - invalid_titles.append((title, rel_path)) - - # Find duplicates - duplicates = {title: paths for title, paths in titles.items() if len(paths) > 1} - - # Print results - console.print("[bold]Doc Card Title Inventory[/bold]") - console.print() - console.print("Titles must be lowercase slugs (e.g., 'grafana-alloy', not 'Grafana Alloy')") - console.print("and unique across the documentation for wiki-link resolution.") - console.print() - - has_errors = False - - # Invalid format table (if any) - if invalid_titles: - has_errors = True - console.print("[bold red]Invalid Title Format[/bold red]") - console.print("Titles must be lowercase with hyphens only (no spaces or uppercase).") - inv_table = Table(show_header=True, header_style="bold") - inv_table.add_column("Title") - inv_table.add_column("File") - - for title, file_path in invalid_titles: - inv_table.add_row(title, file_path) - - console.print(inv_table) - console.print() - - # Duplicates table (if any) - if duplicates: - has_errors = True - console.print("[bold red]Duplicate Titles Found[/bold red]") - dup_table = Table(show_header=True, header_style="bold") - dup_table.add_column("Title") - dup_table.add_column("Files") - - for title in sorted(duplicates.keys()): - paths = duplicates[title] - dup_table.add_row(title, "\n".join(paths)) - - console.print(dup_table) - console.print() - - # All titles table - console.print("[bold]All Titles[/bold]") - all_table = Table(show_header=True, header_style="bold") - all_table.add_column("Title") - all_table.add_column("File") - all_table.add_column("Status") - - for title in sorted(titles.keys()): - paths = titles[title] - is_dup = title in duplicates - is_invalid = not is_valid_slug(title) - - if is_dup: - status = "[red]DUPLICATE[/red]" - elif is_invalid: - status = "[red]INVALID[/red]" - else: - status = "[green]OK[/green]" - - all_table.add_row(title, paths[0], status) - for extra_path in paths[1:]: - all_table.add_row("", extra_path, "") - - console.print(all_table) - - # Summary - console.print() - console.print(f"Total files: {sum(len(p) for p in titles.values())}") - console.print(f"Unique titles: {len(titles)}") - console.print(f"Duplicate titles: {len(duplicates)}") - console.print(f"Invalid format: {len(invalid_titles)}") - - if has_errors: - console.print() - console.print("[bold red]Action required:[/bold red] Fix title issues above.") - return 1 - - return 0 - - -if __name__ == "__main__": - sys.exit(main())