Add Site reference to Output, clean up template url handling

The following changes have been made all with template cleanup in mind:
- Make Output.url relative to the origin root
- Keep weakref to Site in all Output objects.
- Add several helpers to get urls from Site: home_url(), tag_url(tag),
  section_url(section) and tags_url().
- Add helpers to Index to get pagination urls: pagination_url(n)
- Clean up templates by making use of these new facilities
This commit is contained in:
2026-08-14 16:45:16 +02:00
parent 20dd6d9910
commit 58eebac637
13 changed files with 131 additions and 96 deletions
+6 -5
View File
@@ -13,10 +13,11 @@ else:
class Article(Content): class Article(Content):
def __init__(self, source: Path, meta: dict[str, Any]): def __init__(self, site: Site, source: Path, meta: dict[str, Any]):
if "tags" not in meta: if "tags" not in meta:
log.warning("no tags for article in '%s'", source) log.warning("no tags for article in '%s'", source)
super().__init__( super().__init__(
site,
source, source,
meta["section"], meta["section"],
meta["slug"], meta["slug"],
@@ -27,15 +28,15 @@ class Article(Content):
) )
self.title: str = meta["title"] self.title: str = meta["title"]
def write(self, site: Site) -> None: def write(self) -> None:
content, summary = md.parse(self.read_body(), self.summary is None) content, summary = md.parse(self.read_body(), self.summary is None)
if summary is not None: if summary is not None:
self.summary = summary self.summary = summary
template = site.jinja_env.get_template("article.html") template = self.site.jinja_env.get_template("article.html")
html = template.render(article=self, content=content, site=site) html = template.render(page=self, content=content, site=self.site)
out_path = site.output_path / self.destination out_path = self.site.output_path / self.destination
out_path.parent.mkdir(parents=True, exist_ok=True) out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(html, encoding="utf-8") out_path.write_text(html, encoding="utf-8")
log.debug("Rendered %s -> %s", self.source, out_path) log.debug("Rendered %s -> %s", self.source, out_path)
+8 -1
View File
@@ -1,8 +1,14 @@
import re import re
from typing import TYPE_CHECKING
from omicron.ssg.output.output import Output from omicron.ssg.output.output import Output
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
if TYPE_CHECKING:
from omicron.ssg.site import Site
else:
Site = "omicron.ssg.site.Site"
class ContentError(RuntimeError): class ContentError(RuntimeError):
pass pass
@@ -13,6 +19,7 @@ class Content(Output):
def __init__( def __init__(
self, self,
site: Site,
source: Path, source: Path,
section: str, section: str,
slug: str, slug: str,
@@ -21,7 +28,7 @@ class Content(Output):
tags: set[str] | None = None, tags: set[str] | None = None,
summary: str | None = None, summary: str | None = None,
): ):
super().__init__(Content.build_path(section, slug)) super().__init__(site, Content.build_path(section, slug))
if tags is None: if tags is None:
tags = set() tags = set()
self.source = source self.source = source
+8 -3
View File
@@ -1,9 +1,14 @@
import yaml import yaml
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, cast, TYPE_CHECKING
from omicron.ssg.output.content import Content, ContentError from omicron.ssg.output.content import Content, ContentError
from omicron.ssg.output.article import Article from omicron.ssg.output.article import Article
if TYPE_CHECKING:
from omicron.ssg.site import Site
else:
Site = "omicron.ssg.site.Site"
FRONTMATTER_CONTENT = {".md"} FRONTMATTER_CONTENT = {".md"}
CONTENT_EXTENSIONS = FRONTMATTER_CONTENT | {".yml"} CONTENT_EXTENSIONS = FRONTMATTER_CONTENT | {".yml"}
@@ -31,7 +36,7 @@ def read_frontmatter(path: Path) -> dict[str, Any]:
return cast(dict[str, Any], data) return cast(dict[str, Any], data)
def create_content(path: Path) -> Content: def create_content(site: Site, path: Path) -> Content:
if path.suffix in FRONTMATTER_CONTENT: if path.suffix in FRONTMATTER_CONTENT:
meta = read_frontmatter(path) meta = read_frontmatter(path)
elif path.suffix == ".yml": elif path.suffix == ".yml":
@@ -40,6 +45,6 @@ def create_content(path: Path) -> Content:
raise ContentError(f"Unhandled file type for '{path}'") raise ContentError(f"Unhandled file type for '{path}'")
if meta["type"] == "article": if meta["type"] == "article":
return Article(path, meta) return Article(site, path, meta)
else: else:
raise ContentError(f"Invalid type '{meta['type']}' for '{path}'") raise ContentError(f"Invalid type '{meta['type']}' for '{path}'")
+4 -4
View File
@@ -13,12 +13,12 @@ else:
class File(Output): class File(Output):
def __init__(self, path: Path, source: Path): def __init__(self, site: Site, path: Path, source: Path):
super().__init__(path) super().__init__(site, path)
self.source = source self.source = source
def write(self, site: Site) -> None: def write(self) -> None:
dest = site.output_path / self.destination dest = self.site.output_path / self.destination
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(self.source, dest) shutil.copyfile(self.source, dest)
log.debug("Copied %s -> %s", self.source, dest) log.debug("Copied %s -> %s", self.source, dest)
+20 -17
View File
@@ -17,19 +17,24 @@ else:
class Index(Output): class Index(Output):
def __init__( def __init__(
self, self,
site: Site,
kind: Literal["root", "section", "tag"], kind: Literal["root", "section", "tag"],
items: list[Content], items: list[Content],
page_num: int, page_num: int,
total_pages: int, total_pages: int,
label: str | None, label: str | None,
): ):
super().__init__(Index.build_path(kind, page_num, label)) super().__init__(site, Index.build_path(kind, page_num, label))
self.kind = kind self.kind = kind
self.items = items self.items = items
self.page_num = page_num self.page_num = page_num
self.total_pages = total_pages self.total_pages = total_pages
self.label = label self.label = label
def pagination_url(self, n: int) -> str:
path = Index.build_path(self.kind, n, self.label)
return self.site.by_path[path].url
@staticmethod @staticmethod
def build_path( def build_path(
kind: Literal["root", "section", "tag"], kind: Literal["root", "section", "tag"],
@@ -51,23 +56,17 @@ class Index(Output):
path = Path("tags") / label / file path = Path("tags") / label / file
return path return path
def write(self, site: Site) -> None: def write(self) -> None:
template = site.jinja_env.get_template("index.html") template = self.site.jinja_env.get_template("index.html")
html = template.render( html = template.render(site=self.site, page=self)
site=site, dest = self.site.output_path / self.destination
items=self.items,
page_num=self.page_num,
total_pages=self.total_pages,
kind=self.kind,
label=self.label,
)
dest = site.output_path / self.destination
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(html, encoding="utf-8") dest.write_text(html, encoding="utf-8")
log.debug("Wrote %s", dest) log.debug("Wrote %s", dest)
def make_index_pages( def make_index_pages(
site: Site,
items: list[Content], items: list[Content],
kind: Literal["root", "section", "tag"], kind: Literal["root", "section", "tag"],
label: str | None, label: str | None,
@@ -80,19 +79,23 @@ def make_index_pages(
kind, kind,
f" ({label})" if label else "", f" ({label})" if label else "",
) )
return [Index(kind, [], 1, 1, label)] return [Index(site, kind, [], 1, 1, label)]
num_pages = ceil(len(sorted_items) / items_per_page) num_pages = ceil(len(sorted_items) / items_per_page)
return [ return [
Index(kind, list(page_items), i + 1, num_pages, label) Index(site, kind, list(page_items), i + 1, num_pages, label)
for i, page_items in enumerate(batched(sorted_items, items_per_page)) for i, page_items in enumerate(batched(sorted_items, items_per_page))
] ]
def discover_index(site: Site) -> list[Index]: def discover_index(site: Site) -> list[Index]:
outputs: list[Index] = [] outputs: list[Index] = []
outputs.extend(make_index_pages(site.content, "root", None, site.items_per_page)) outputs.extend(
make_index_pages(site, site.content, "root", None, site.items_per_page)
)
for section, items in site.by_section.items(): for section, items in site.by_section.items():
outputs.extend(make_index_pages(items, "section", section, site.items_per_page)) outputs.extend(
make_index_pages(site, items, "section", section, site.items_per_page)
)
for tag, items in site.by_tag.items(): for tag, items in site.by_tag.items():
outputs.extend(make_index_pages(items, "tag", tag, site.items_per_page)) outputs.extend(make_index_pages(site, items, "tag", tag, site.items_per_page))
return outputs return outputs
+4 -4
View File
@@ -12,12 +12,12 @@ else:
class Memory(Output): class Memory(Output):
def __init__(self, destination: Path, content: str): def __init__(self, site: Site, destination: Path, content: str):
super().__init__(destination) super().__init__(site, destination)
self.content = content self.content = content
def write(self, site: Site) -> None: def write(self) -> None:
dest = site.output_path / self.destination dest = self.site.output_path / self.destination
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(self.content, encoding="utf-8") dest.write_text(self.content, encoding="utf-8")
log.debug("Wrote %s", dest) log.debug("Wrote %s", dest)
+15 -6
View File
@@ -1,3 +1,4 @@
import weakref
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
@@ -9,14 +10,22 @@ else:
class Output(ABC): class Output(ABC):
def __init__(self, destination: Path): def __init__(self, site: Site, destination: Path):
self._site_ref = weakref.ref(site)
self.destination = destination self.destination = destination
url = "/" + destination.as_posix() url = site.base_dir + "/" + destination.as_posix()
url = url.removesuffix("/index.html") url = url.removesuffix("/index.html") or "/"
if url == "":
url = "/"
self.url = url self.url = url
@property
def site(self) -> Site:
obj = self._site_ref()
if obj is None:
raise ReferenceError(
f"Site for output {self.destination} has been garbage collected"
)
return obj
@abstractmethod @abstractmethod
def write(self, site: Site) -> None: def write(self) -> None:
pass pass
+9 -7
View File
@@ -12,18 +12,20 @@ else:
class Tags(Output): class Tags(Output):
def __init__(self, tags: list[tuple[str, int]]): PATH = Path("tags/index.html")
super().__init__(Path("tags/index.html"))
def __init__(self, site: Site, tags: list[tuple[str, int]]):
super().__init__(site, Tags.PATH)
self.tags = tags self.tags = tags
def write(self, site: Site) -> None: def write(self) -> None:
template = site.jinja_env.get_template("tags.html") template = self.site.jinja_env.get_template("tags.html")
html = template.render(site=site, tags=self.tags) html = template.render(site=self.site, page=self)
dest = site.output_path / self.destination dest = self.site.output_path / self.destination
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(html, encoding="utf-8") dest.write_text(html, encoding="utf-8")
log.debug("Wrote %s", dest) log.debug("Wrote %s", dest)
def discover_tags(site: Site) -> Tags: def discover_tags(site: Site) -> Tags:
return Tags(site.tags_by_count) return Tags(site, site.tags_by_count)
+21 -5
View File
@@ -9,6 +9,8 @@ from omicron.ssg.output import (
is_content, is_content,
create_content, create_content,
File, File,
Index,
Tags,
Memory, Memory,
discover_index, discover_index,
discover_tags, discover_tags,
@@ -98,6 +100,20 @@ class Site:
) )
self.by_path[output.destination] = output self.by_path[output.destination] = output
def home_url(self) -> str:
return self.base_dir or "/"
def section_url(self, section: str) -> str:
path = Index.build_path("section", 1, section)
return self.by_path[path].url
def tag_url(self, tag: str) -> str:
path = Index.build_path("tag", 1, tag)
return self.by_path[path].url
def tags_url(self) -> str:
return self.by_path[Tags.PATH].url
def update_tags_by_count(self) -> None: def update_tags_by_count(self) -> None:
tags = [(tag, len(items)) for tag, items in self.by_tag.items()] tags = [(tag, len(items)) for tag, items in self.by_tag.items()]
tags.sort(key=lambda x: x[1], reverse=True) tags.sort(key=lambda x: x[1], reverse=True)
@@ -110,7 +126,7 @@ class Site:
for file in assets_path.rglob("*"): for file in assets_path.rglob("*"):
if file.is_file(): if file.is_file():
path = Path("assets") / file.relative_to(assets_path) path = Path("assets") / file.relative_to(assets_path)
asset = File(path, file) asset = File(self, path, file)
self.add_by_path(asset) self.add_by_path(asset)
self.other_outputs.append(asset) self.other_outputs.append(asset)
log.debug("Discovered template asset %s", path) log.debug("Discovered template asset %s", path)
@@ -118,13 +134,13 @@ class Site:
def discover(self) -> None: def discover(self) -> None:
log.info("Discovering content...") log.info("Discovering content...")
self.discover_assets() self.discover_assets()
pygments = Memory(Path("assets/pygments.css"), highlight_style()) pygments = Memory(self, Path("assets/pygments.css"), highlight_style())
self.other_outputs.append(pygments) self.other_outputs.append(pygments)
self.add_by_path(pygments) self.add_by_path(pygments)
content_path = self.site_path / "content" content_path = self.site_path / "content"
for file in content_path.rglob("*"): for file in content_path.rglob("*"):
if file.is_file() and is_content(file): if file.is_file() and is_content(file):
content = create_content(file) content = create_content(self, file)
self.add_by_path(content) self.add_by_path(content)
self.add_by_section(content) self.add_by_section(content)
self.add_by_tag(content) self.add_by_tag(content)
@@ -149,6 +165,6 @@ class Site:
self.discover() self.discover()
self.output_path.mkdir(parents=True, exist_ok=True) self.output_path.mkdir(parents=True, exist_ok=True)
for content in self.content: for content in self.content:
content.write(self) content.write()
for output in self.other_outputs: for output in self.other_outputs:
output.write(self) output.write()
+11 -13
View File
@@ -1,31 +1,29 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ article.title }} — {{ site.name }}{% endblock %} {% block title %}{{ page.title }} — {{ site.name }}{% endblock %}
{% block head %}<link rel="canonical" href="{{ site.origin }}{{ site.base_dir }}{{ article.url }}">{% endblock %}
{% block breadcrumb %} {% block breadcrumb %}
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<a href="{{ site.base_dir or "/" }}">{{ site.name }}</a> <a href="{{ site.home_url() }}">{{ site.name }}</a>
<a href="{{ site.base_dir }}/{{ article.section }}">{{ article.section }}</a> <a href="{{ site.section_url(page.section) }}">{{ page.section }}</a>
{{ article.title }} {{ page.title }}
</nav> </nav>
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<article> <article>
<header> <header>
<h1>{{ article.title }}</h1> <h1>{{ page.title }}</h1>
<p> <p>
<time datetime="{{ article.created }}">{{ article.created }}</time> <time datetime="{{ page.created }}">{{ page.created }}</time>
{% if article.updated %} {% if page.updated %}
· Updated <time datetime="{{ article.updated }}">{{ article.updated }}</time> · Updated <time datetime="{{ page.updated }}">{{ page.updated }}</time>
{% endif %} {% endif %}
</p> </p>
{% if article.tags %} {% if page.tags %}
<ul> <ul>
{% for tag in article.tags | sort %} {% for tag in page.tags | sort %}
<li><a href="{{ site.base_dir }}/tags/{{ tag }}">{{ tag }}</a></li> <li><a href="{{ site.tag_url(tag) }}">{{ tag }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
+2 -1
View File
@@ -6,11 +6,12 @@
<title>{% block title %}{{ site.name }}{% endblock %}</title> <title>{% block title %}{{ site.name }}{% endblock %}</title>
<link rel="stylesheet" href="{{ site.base_dir }}/assets/style.css"> <link rel="stylesheet" href="{{ site.base_dir }}/assets/style.css">
<link rel="stylesheet" href="{{ site.base_dir }}/assets/pygments.css"> <link rel="stylesheet" href="{{ site.base_dir }}/assets/pygments.css">
<link rel="canonical" href="{{ site.origin }}{{ page.url }}">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<header> <header>
{% block breadcrumb %}<a href="{{ site.base_dir or "/" }}">{{ site.name }}</a>{% endblock %} {% block breadcrumb %}<a href="{{ site.home_url() }}">{{ site.name }}</a>{% endblock %}
</header> </header>
<main> <main>
{% block main %}{% endblock %} {% block main %}{% endblock %}
+20 -23
View File
@@ -1,25 +1,22 @@
{% extends "base.html" %} {% extends "base.html" %}
{% set base_path = ("/" + label) if kind == "section" else (("/tags/" + label) if kind == "tag" else "") %}
{% set heading = label or "" %}
{% block title %}{% if heading %}{{ heading }} — {% endif %}{{ site.name }}{% if page_num > 1 %} — Page {{ page_num }}{% endif %}{% endblock %} {% block title %}{% if page.label %}{{ page.label }} — {% endif %}{{ site.name }}{% if page.page_num > 1 %} — Page {{ page.page_num }}{% endif %}{% endblock %}
{% block head %} {% block head %}
<link rel="canonical" href="{{ site.origin }}{{ site.base_dir }}{% if page_num == 1 %}{{ base_path or "/" }}{% else %}{{ base_path }}/page-{{ page_num }}.html{% endif %}"> {% if page.page_num > 1 %}<meta name="robots" content="noindex, follow">{% endif %}
{% if page_num > 1 %}<meta name="robots" content="noindex, follow">{% endif %}
{% endblock %} {% endblock %}
{% block breadcrumb %} {% block breadcrumb %}
{% if kind == "section" %} {% if page.kind == "section" %}
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<a href="{{ site.base_dir or "/" }}">{{ site.name }}</a> <a href="{{ site.home_url() }}">{{ site.name }}</a>
{{ label }} {{ page.label }}
</nav> </nav>
{% elif kind == "tag" %} {% elif page.kind == "tag" %}
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<a href="{{ site.base_dir or "/" }}">{{ site.name }}</a> <a href="{{ site.home_url() }}">{{ site.name }}</a>
<a href="{{ site.base_dir }}/tags">Tags</a> <a href="{{ site.tags_url() }}">Tags</a>
{{ label }} {{ page.label }}
</nav> </nav>
{% else %} {% else %}
{{ super() }} {{ super() }}
@@ -27,28 +24,28 @@
{% endblock %} {% endblock %}
{% block main %} {% block main %}
{% if heading %}<h1>{{ heading }}</h1>{% endif %} {% if page.label %}<h1>{{ page.label }}</h1>{% endif %}
{% for item in items %} {% for item in page.items %}
<article> <article>
<header> <header>
<h2><a href="{{ site.base_dir }}{{ item.url }}">{{ item.title }}</a></h2> <h2><a href="{{ item.url }}">{{ item.title }}</a></h2>
<time datetime="{{ item.created }}">{{ item.created }}</time> <time datetime="{{ item.created }}">{{ item.created }}</time>
</header> </header>
{% if item.summary %}<p>{{ item.summary }}</p>{% endif %} {% if item.summary %}<p>{{ item.summary }}</p>{% endif %}
</article> </article>
{% endfor %} {% endfor %}
{% if total_pages > 1 %} {% if page.total_pages > 1 %}
<nav> <nav>
{% if page_num > 1 %} {% if page.page_num > 1 %}
<a href="{{ site.base_dir }}{% if page_num - 1 == 1 %}{{ base_path or "/" }}{% else %}{{ base_path }}/page-{{ page_num - 1 }}.html{% endif %}">← Newer</a> <a href="{{ page.pagination_url(page.page_num - 1) }}">← Newer</a>
{% endif %} {% endif %}
{% for p in range(1, total_pages + 1) %} {% for p in range(1, page.total_pages + 1) %}
{% if p == page_num %}<strong>{{ p }}</strong> {% if p == page.page_num %}<strong>{{ p }}</strong>
{% else %}<a href="{{ site.base_dir }}{% if p == 1 %}{{ base_path or "/" }}{% else %}{{ base_path }}/page-{{ p }}.html{% endif %}">{{ p }}</a> {% else %}<a href="{{ page.pagination_url(p) }}">{{ p }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if page_num < total_pages %} {% if page.page_num < page.total_pages %}
<a href="{{ site.base_dir }}{{ base_path }}/page-{{ page_num + 1 }}.html">Older →</a> <a href="{{ page.pagination_url(page.page_num + 1) }}">Older →</a>
{% endif %} {% endif %}
</nav> </nav>
{% endif %} {% endif %}
+3 -7
View File
@@ -2,13 +2,9 @@
{% block title %}Tags — {{ site.name }}{% endblock %} {% block title %}Tags — {{ site.name }}{% endblock %}
{% block head %}
<link rel="canonical" href="{{ site.origin }}{{ site.base_dir }}/tags">
{% endblock %}
{% block breadcrumb %} {% block breadcrumb %}
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<a href="{{ site.base_dir or "/" }}">{{ site.name }}</a> <a href="{{ site.home_url() }}">{{ site.name }}</a>
Tags Tags
</nav> </nav>
{% endblock %} {% endblock %}
@@ -16,8 +12,8 @@
{% block main %} {% block main %}
<h1>Tags</h1> <h1>Tags</h1>
<ul> <ul>
{% for tag, count in tags %} {% for tag, count in page.tags %}
<li><a href="{{ site.base_dir }}/tags/{{ tag }}">{{ tag }}</a> ({{ count }})</li> <li><a href="{{ site.tag_url(tag) }}">{{ tag }}</a> ({{ count }})</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endblock %} {% endblock %}