Remove title slug check and test duplicate titles (#116)
## Summary - Remove `docs-check-titles` pre-commit hook and mise task — wiki-links resolve by filename stem, not frontmatter title, so slug-format titles and uniqueness aren't needed - Add two test cards (`title-test-alpha`, `title-test-beta`) with identical `title: Title Test Card` to verify duplicate titles don't break Quartz or obsidian.nvim - Retitle `index.md` from `blumeops-documentation` to `BlumeOps` - Add GitHub and Forgejo repo links to homepage intro ## Test plan - [ ] Deploy to docs site and verify both test cards render and cross-link correctly - [ ] Verify homepage title renders as "BlumeOps" - [ ] Verify repo links on homepage work - [ ] After confirming, remove test cards in a follow-up 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/116
This commit is contained in:
parent
3f5017f732
commit
a7d6d44d3d
8 changed files with 34 additions and 180 deletions
|
|
@ -92,12 +92,6 @@ repos:
|
||||||
# Documentation validation
|
# Documentation validation
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
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
|
- id: docs-check-filenames
|
||||||
name: docs-check-filenames
|
name: docs-check-filenames
|
||||||
entry: mise run docs-check-filenames
|
entry: mise run docs-check-filenames
|
||||||
|
|
|
||||||
1
docs/changelog.d/feature-docs-title-cleanup.doc.md
Normal file
1
docs/changelog.d/feature-docs-title-cleanup.doc.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Remove docs-check-titles pre-commit hook, add repo links to homepage, and test duplicate frontmatter titles.
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
title: blumeops-documentation
|
title: BlumeOps
|
||||||
aliases: []
|
aliases: []
|
||||||
id: index
|
id: index
|
||||||
tags: []
|
tags: []
|
||||||
|
|
@ -14,11 +14,11 @@ infrastructure.
|
||||||
## What is BlumeOps?
|
## What is BlumeOps?
|
||||||
|
|
||||||
BlumeOps is my personal homelab infrastructure managed entirely through code.
|
BlumeOps is my personal homelab infrastructure managed entirely through code.
|
||||||
Everything lives in a single git repository, from service configs to deployment
|
Everything lives in a [single git repository](https://github.com/eblume/blumeops), from service configs to
|
||||||
automation. Even the [[forgejo]] instance that hosts this repo is defined
|
deployment automation. Even the [[forgejo]] instance that [hosts this repo](https://forge.ops.eblu.me/eblume/blumeops)
|
||||||
within it, making BlumeOps fully self-hosting. It's a digital life raft I built
|
is defined within it, making BlumeOps fully self-hosting. It's a digital life
|
||||||
for myself as I went, and you can see it all from within your editor of choice.
|
raft I built for myself as I went, and you can see it all from within your
|
||||||
(I recommend vim.)
|
editor of choice. (I recommend vim.)
|
||||||
|
|
||||||
These services run on my home [[hosts|infrastructure]], primarily an m1 mac
|
These services run on my home [[hosts|infrastructure]], primarily an m1 mac
|
||||||
mini named [[indri]] and a Synology NAS called [[sifaka]]. The infrastructure
|
mini named [[indri]] and a Synology NAS called [[sifaka]]. The infrastructure
|
||||||
|
|
|
||||||
13
docs/reference/infrastructure/title-test-alpha.md
Normal file
13
docs/reference/infrastructure/title-test-alpha.md
Normal file
|
|
@ -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.
|
||||||
13
docs/reference/infrastructure/title-test-beta.md
Normal file
13
docs/reference/infrastructure/title-test-beta.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -46,6 +46,7 @@ Host inventory and network configuration.
|
||||||
- [[tailscale]] - ACLs, groups, tags
|
- [[tailscale]] - ACLs, groups, tags
|
||||||
- [[gandi]] - DNS hosting for `eblu.me`
|
- [[gandi]] - DNS hosting for `eblu.me`
|
||||||
- [[routing|Routing]] - DNS domains, port mappings
|
- [[routing|Routing]] - DNS domains, port mappings
|
||||||
|
- [[title-test-alpha]] / [[title-test-beta]] - Duplicate title test (temporary)
|
||||||
|
|
||||||
## Kubernetes
|
## Kubernetes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| `tailnet-up` | Apply Tailscale ACL changes via Pulumi |
|
||||||
| `docs-check-links` | Validate wiki-links in documentation (includes orphan detection) |
|
| `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-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-check-filenames` | Check for duplicate doc filenames |
|
||||||
| `docs-review-stale` | Report docs by last-modified date, highlight stale ones |
|
| `docs-review-stale` | Report docs by last-modified date, highlight stale ones |
|
||||||
| `docs-review-tags` | Print frontmatter tag inventory across all docs |
|
| `docs-review-tags` | Print frontmatter tag inventory across all docs |
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue