Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ When decomposing or extending templates, maintain these blocks so user template

## Feature Implementation Workflow

Phase B work is one feature per PR. Implement → open PR → get reviewed → merge → move to next feature.

**REQUIRED** — every feature implementation MUST follow these steps:

1. **Before starting**: Check `plans/features/README.md` and the feature's spec doc (`plans/features/NN-*.md`)
Expand All @@ -87,3 +89,4 @@ When decomposing or extending templates, maintain these blocks so user template
- `plans/implementation.md` — mark checklist items as complete
- `plans/features/README.md` — update feature status and Quartz reference checklist
- Feature spec doc (if one exists) — update status to reflect completion
4. **Open a PR** and wait for review before starting the next feature
2 changes: 1 addition & 1 deletion plans/features/17-seo-meta.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Frontmatter-driven meta tags and Open Graph support.

## Status: Not Started
## Status: Complete ✅

## Goal

Expand Down
93 changes: 93 additions & 0 deletions plans/features/N8-tag-index-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Feature N8: Tag Index Pages

Generate `/tags/<tag>/` listing pages for all unique tags found in content.

## Status: Not Started (Phase B, Batch 1)

## Goal

Every unique tag across all content gets a dedicated listing page. Tag badges on content pages link to their index. A root `/tags/` page lists all tags.

## Prerequisite

Tag display (N7) is complete — tags are normalized (leading `#` stripped, lowercased) and rendered on pages.

## Output

```
_site/
tags/
index.html ← all tags, each linking to its listing
python/
index.html ← all pages tagged "python"
obsidian/
index.html ← all pages tagged "obsidian"
```

## Tag Normalization

Use the same normalization as N7: strip leading `#`, lowercase. Tags `Python`, `#python`, and `python` all map to the slug `python`.

## Template

`templates/tag_index.html` — listing page for a single tag. Shows tag name and a list of content pages (title + path).

`templates/tags_root.html` — root `/tags/` page listing all tags with page counts.

Both extend the active layout.

## Tag Badge Links

Update the tag badge rendering in `after_heading` to link each badge to `/tags/<slug>/`.

## Config

No required config. Optional:
```toml
[theme]
tag_index = true # default: true — set false to disable tag pages entirely
```

## Implementation Plan

### 1. Tag Collection

After content is loaded, collect all tags across all pages:

```python
def collect_tags(store: ContentStore) -> dict[str, list[Page]]:
"""Return mapping of normalized tag slug → list of pages."""
```

### 2. Tag Page Generation

In `output/builder.py`, after rendering content pages:

```python
for tag_slug, pages in tags.items():
render_tag_page(tag_slug, pages, output_dir)
render_tags_root(tags, output_dir)
```

### 3. Templates

- `templates/tag_index.html` — single tag listing
- `templates/tags_root.html` — all tags overview

### 4. Link Tag Badges

Update the `after_heading` block (or wherever tags are rendered) so each badge links to `/tags/<slug>/`.

## Key Files to Create/Modify

- `output/builder.py` — tag collection + page generation
- `templates/tag_index.html` — new
- `templates/tags_root.html` — new
- `templates/components/tags.html` (or wherever tag badges render) — add links

## Verification

- Build a vault with multiple tags → `/tags/` and `/tags/<tag>/` pages generated
- Tag badges on content pages link to the correct index
- Unnormalized tags (`#Python`, `Python`) all resolve to the same `/tags/python/` page
- Tag with no pages is not generated
4 changes: 2 additions & 2 deletions plans/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ The [PyOhio static website](https://github.com/pyohio/static-website) (Astro + P
| 14 | [Collections](14-collections.md) | ❌ | B | Unified content model, progressive collections |
| 15 | [Build Hooks](15-build-hooks.md) | ❌ | B | Pre/post-build shell commands |
| 16 | [Static Assets](16-static-assets.md) | ❌ | B | Custom CSS & JS inclusion |
| 17 | [SEO & Meta Tags](17-seo-meta.md) | | B | Frontmatter-driven meta, OG tags |
| 17 | [SEO & Meta Tags](17-seo-meta.md) | | B | Frontmatter-driven meta, OG tags |
| 18 | [Accessibility](18-accessibility.md) | ✅ | A | Skip links, ARIA, focus styles |
| N6 | Broken Link Handling | ✅ | A | Visual indication + build warnings |
| N7 | Tag Display | ✅ | A | Show frontmatter tags on pages |
| N8 | Tag Index Pages | ❌ | B | Generate `/tags/<tag>/` listing pages |
| N8 | [Tag Index Pages](N8-tag-index-pages.md) | ❌ | B | Generate `/tags/<tag>/` listing pages |
| N9 | Template Decomposition | ✅ | A | Named blocks as customization hooks in page templates |
| N10 | Newline Handling | ✅ | A | Obsidian-style single newline → `<br>` |
| N11 | [Config Validation](N11-config-validation.md) | ❌ | B | `validate` command, unknown-key warnings, theme manifest |
Expand Down
110 changes: 110 additions & 0 deletions plans/features/og-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# OG Image Generation — Options & Recommendation

## Status: Future Feature (Phase C or later)

## Context

Feature 17 (SEO & Meta Tags) is complete. The `og_image` frontmatter field and `site.og_image` config fallback are wired into `templates/components/meta.html`. Currently the field expects a **pre-existing image path** — users must provide OG images manually.

The goal is to **generate OG images automatically at build time** from page data (title, description, tags, etc.), so every page gets a social card without manual effort.

This feature depends on build hooks (Feature 15, Phase B Batch 4) being implemented first.

---

## Key Architectural Context

- Page data available for generation: `page.title`, `page.frontmatter.get("description")`, `page.frontmatter.get("tags")`, `page.slug`, `page.modified`
- `og_image` frontmatter field already consumed by `meta.html` — populating it before rendering means it just works
- The planned `post_collect` build hook stage (Feature 15) is explicitly designed for this use case: content is collected and exported to `.rockgarden/content.json`, hook scripts run before HTML rendering
- Node.js is already a project dependency (Tailwind CLI)
- Relevant files: `src/rockgarden/output/builder.py`, `src/rockgarden/config.py`, `src/rockgarden/content/models.py`, `src/rockgarden/templates/components/meta.html`

---

## Options

### Option A: Pillow (pure Python, built-in)

Render title + description onto a background image using Pillow's text drawing API.

- **Pros**: Zero extra deps (or Pillow as an optional dep), pure Python, fast, CI-friendly
- **Cons**: Low-level layout — wrapping long titles, centering, multi-line text require manual math. Hard to make look polished.
- **Best for**: Simple, consistent cards (title + site name on a branded background)
- Pillow is not currently a dependency — would need to be added as optional

### Option B: SVG template → CairoSVG

User provides a Jinja2 SVG template with `{{ page.title }}` etc. Python renders it and converts to PNG via CairoSVG.

- **Pros**: SVG handles layout/design — designers can work in Inkscape/Figma. Text wrapping is natural. Good quality output.
- **Cons**: Requires Cairo system library (`brew install cairo` / `apt install libcairo2`) — cross-platform friction.
- **Best for**: Sites where the design matters and users are comfortable with SVG

### Option C: Satori + Node subprocess (best quality)

Vercel's approach: render HTML/CSS to SVG via Satori (pure JS, no browser), convert SVG → PNG via resvg-js. rockgarden would ship a small Node script.

```bash
node scripts/generate_og.js --content .rockgarden/content.json --output _site/og/
```

- **Pros**: Highest quality output (flexbox layout), fast (milliseconds per image), headless/CI-friendly. Node.js already present for Tailwind.
- **Cons**: Adds JS complexity to what is otherwise a Python tool; users need `npm install`.
- **Best for**: Sites needing polished, design-forward cards (e.g. PyOhio)

### Option D: Post-collect hook pattern (most flexible, no built-in)

Implement build hooks (Feature 15) and document the pattern. rockgarden exports `.rockgarden/content.json`; users write their own generation script.

```toml
[hooks]
post_collect = ["python scripts/generate_og_images.py"]
```

- **Pros**: Maximum flexibility — users choose any library. No new core deps.
- **Cons**: Not "batteries included."
- **Best for**: Power users and complex sites already running a build pipeline

---

## Recommendation

**Two-tier approach:**

1. **Phase B (with hooks)**: Implement build hooks (Feature 15). Document the `post_collect` hook pattern for OG image generation. Provide example scripts — one Pillow-based (simple), one Satori-based (polished). Unblocks PyOhio and advanced users immediately with no new core deps.

2. **Phase C (built-in)**: Add an opt-in built-in generator using **SVG template → CairoSVG**:
```toml
[og_images]
enabled = true
template = "_themes/mysite/og-template.svg"
output_dir = "og" # written to _site/og/<slug>.png
```
SVG is preferred over Pillow because layout and design are far easier to express there.

---

## Integration Point

```
load_content()
→ export .rockgarden/content.json
→ [post_collect hook OR built-in og_images step]
→ write _site/og/<slug>.png per page
→ update page.frontmatter["og_image"] = "/og/<slug>/"
→ render_page() → meta.html picks up og_image automatically
```

No changes to `meta.html` needed — it already conditionally renders `og:image`.

---

## Dependencies

| Approach | New Python dep | New system dep | New Node dep |
|---|---|---|---|
| Pillow | `pillow` (optional) | None | None |
| CairoSVG | `cairosvg` (optional) | `libcairo2` | None |
| Satori | None | None | `satori`, `@resvg/resvg-js` |
| Hook pattern | None | None | None |
90 changes: 76 additions & 14 deletions plans/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,79 @@ After each step, verify incrementally:
- [x] Build timing in CLI output (`Built N pages in X.Xs → /path`)
- Detailed error validation and per-phase metrics deferred to future improvement

### Future (Phase B+)
- [ ] **Tag Index Pages (N8)**: Generate `/tags/<tag>/` pages listing all content with a given tag
- Depends on tag display (above) for normalization
- Pairs naturally with collections work

- [ ] **Graph View**: Interactive visualization of page connections
- Requirements to be workshopped and defined

- [ ] Config validation command + theme manifest (N11)
- [ ] Collections and content models (Feature 14)
- [ ] Build hooks (Feature 15)
- [ ] Base path prefix support (Feature 12)
- [ ] Static asset inclusion (Feature 16)
- [ ] SEO & meta tags (Feature 17)
---

## Phase B: General SSG / PyOhio (0.9 Prerelease)

Work in batches, one feature per PR.

### Batch 1 — Independent Quick Wins

#### N8: Tag Index Pages
- [ ] Generate `/tags/<tag>/` listing pages for all unique tags
- [ ] Link tag badges on pages to their index page
- [ ] Add tag index entry to nav (optional, config-controlled)
- [ ] Generate tag index root page (`/tags/`) listing all tags

#### Feature 17: SEO & Meta Tags ✅
- [x] Add `description` and `og_image` to `SiteConfig`
- [x] Create `templates/components/meta.html` with conditional meta tag rendering
- [x] Include meta component in `base.html` `<head>`

### Batch 2 — Base Path Prefix

#### Feature 12: Base Path Prefix
- [ ] Confirm `base_url` handling in `SiteConfig` (already used for sitemap)
- [ ] Update URL generation helpers to include base path
- [ ] Update all templates to use `base_url` for asset and internal link references
- [ ] Update search index URL generation
- [ ] Verify wiki-link resolution is unaffected

### Batch 3 — Theming Foundation

#### Feature 10B: Layout System
- [ ] Refactor `base.html` to minimal HTML skeleton (head, body wrapper, script injection only)
- [ ] Create `layouts/docs.html` (extract current sidebar/drawer layout from `base.html`)
- [ ] Add `resolve_layout()` to `render/engine.py`
- [ ] Update `render_page()` to inject `layout_template` into render context
- [ ] Update `page.html` and `folder_index.html` to `{% extends layout_template %}`
- [ ] Add `theme.default_layout` config field

#### Feature 16: Static Assets (CSS & JS)
- [ ] Discover files in `_styles/` and `_scripts/` at build time
- [ ] Copy to `_site/styles/` and `_site/scripts/`
- [ ] Inject `<link>` and `<script>` tags in `base.html`

#### Feature 10C: Theme Export CLI
- [ ] Add `theme` command group to `cli.py`
- [ ] Implement `rockgarden theme export` — copies bundled theme to `_themes/default/`

### Batch 4 — Build Pipeline

#### Feature 15: Build Hooks
- [ ] Add `[hooks]` section to config (`pre_build`, `post_collect`, `post_build`)
- [ ] Export content store to `.rockgarden/content.json` after collection
- [ ] Execute hook shell commands at each stage with error handling

#### Feature 14: Collections
- [ ] Collection-aware `ContentStore` (`list_content("name")`, `get_content("name", slug=...)`)
- [ ] Config: `[[collections]]` with `name` and `source` fields
- [ ] Named collection carves its directory out of the default collection
- [ ] Nested collection support (content in `a/b/` belongs to both `b` and `a`)
- [ ] Optional model/schema (`[models.x]` with `fields`)
- [ ] Non-markdown format loading (YAML/JSON/TOML) when collection config enables it
- [ ] Custom template and URL pattern per collection
- [ ] Collection page generation controls (`pages = false`, `nav = true`)

### Anytime: N11 Config Validation

#### Feature N11: Config Validation
- [ ] New `validation.py` with `validate_config()` and `load_theme_manifest()`
- [ ] `rockgarden validate` CLI command (exits 1 on errors, 0 on warnings)
- [ ] Known-key validation derived from dataclass field names
- [ ] Theme manifest (`_themes/<name>/theme.toml`) loading and required-key check

### Future / Deferred

- [ ] **Graph View**: Interactive visualization of page connections — requirements TBD
- [ ] **Tag Index in Nav**: Defer to after N8 is done, decide based on usage
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ select = [
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
filterwarnings = ["ignore::DeprecationWarning:frontmatter"]

[tool.commitizen]
name = "cz_conventional_commits"
Expand Down
2 changes: 2 additions & 0 deletions src/rockgarden/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class SiteConfig(BaseModel):
"""Site-level configuration."""

title: str = "My Site"
description: str = ""
og_image: str = ""
source: Path = Path(".")
output: Path = Path("_site")
clean_urls: bool = True
Expand Down
4 changes: 4 additions & 0 deletions src/rockgarden/output/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ def build_site(config: Config, source: Path, output: Path) -> BuildResult:

site_config = {
"title": config.site.title,
"description": config.site.description,
"og_image": config.site.og_image,
"base_url": config.site.base_url,
"clean_urls": config.site.clean_urls,
"nav": nav_tree,
"nav_default_state": config.theme.nav_default_state,
"daisyui_theme": config.theme.daisyui_default,
Expand Down
1 change: 1 addition & 0 deletions src/rockgarden/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ page.title }}{% if site.title %} - {{ site.title }}{% endif %}{% endblock %}</title>
{% include "components/meta.html" %}
<link href="/_static/rockgarden.css{% if site.cache_hash %}?v={{ site.cache_hash }}{% endif %}" rel="stylesheet" type="text/css" />
{% if site.search_enabled %}
<script src="/_static/lunr.min.js{% if site.cache_hash %}?v={{ site.cache_hash }}{% endif %}"></script>
Expand Down
Loading