From c6dd2785af8b7b68b50cb6e9f245b660e3258e99 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 15:09:23 -0400 Subject: [PATCH 01/19] added .kakrc to manage cwd in editor --- .kakrc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .kakrc diff --git a/.kakrc b/.kakrc new file mode 100644 index 0000000..7a60b1f --- /dev/null +++ b/.kakrc @@ -0,0 +1,21 @@ +# commands to edit important files in the root +declare-option str project_root %sh{ git rev-parse --show-toplevel } + +define-command -params 1 root-edit %{ + edit %exp{%opt{project_root}/%arg{1}} +} + +define-command just %{ + root-edit justfile +} +define-command pyproject %{ + root-edit pyproject.toml +} +define-command readme %{ + root-edit README.md +} + +# change working directory to the package +hook global -once BufCreate .* %{ + change-directory %exp{%opt{project_root}/src/zona} +} From fa10a813f214af66622025044e412735cbe4729d Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 16:48:03 -0400 Subject: [PATCH 02/19] add to .kakrc --- .kakrc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.kakrc b/.kakrc index 7a60b1f..688bcbe 100644 --- a/.kakrc +++ b/.kakrc @@ -15,6 +15,10 @@ define-command readme %{ root-edit README.md } +define-command kakrc %{ + root-edit .kakrc +} + # change working directory to the package hook global -once BufCreate .* %{ change-directory %exp{%opt{project_root}/src/zona} From bafe70ed3728682af41597e908b7b5ad97ef496b Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 16:52:58 -0400 Subject: [PATCH 03/19] fix post-nav button order Navigation now follows newer/older logic. click right to go older, left to go newer. --- src/zona/builder.py | 14 ++++++++++---- src/zona/data/templates/post_nav.html | 12 +++++++----- src/zona/models.py | 4 ++-- src/zona/templates.py | 9 ++++----- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/zona/builder.py b/src/zona/builder.py index 5c8af6b..416edc8 100644 --- a/src/zona/builder.py +++ b/src/zona/builder.py @@ -99,6 +99,8 @@ class ZonaBuilder: def _build(self): assert self.items + # sort according to date + # descending order post_list: list[Item] = sorted( [item for item in self.items if item.post], key=lambda item: item.metadata.date @@ -106,12 +108,16 @@ class ZonaBuilder: else date.min, reverse=True, ) + # number of posts posts = len(post_list) + # link post chronology for i, item in enumerate(post_list): - prev = post_list[i - 1] if i > 0 else None - next = post_list[i + 1] if i < posts - 2 else None - item.previous = prev - item.next = next + # prev: older post + older = post_list[i + 1] if i + 1 < posts else None + # next: newer post + newer = post_list[i - 1] if i > 0 else None + item.older = older + item.newer = newer templater = Templater( config=self.config, diff --git a/src/zona/data/templates/post_nav.html b/src/zona/data/templates/post_nav.html index 86ac330..1e96ff5 100644 --- a/src/zona/data/templates/post_nav.html +++ b/src/zona/data/templates/post_nav.html @@ -1,9 +1,11 @@
- <{% if previous %}prev{% endif %}{% if previous and next %}|{% endif %}{% if next %}next{% endif - %}> + <{% if newer %}newr{% + else %}null{% endif %}{% if older %}oldr{% else %}null{% endif %}>
diff --git a/src/zona/models.py b/src/zona/models.py index d009476..7f67015 100644 --- a/src/zona/models.py +++ b/src/zona/models.py @@ -23,8 +23,8 @@ class Item: type: ItemType | None = None copy: bool = True post: bool = False - next: Item | None = None - previous: Item | None = None + newer: Item | None = None + older: Item | None = None # @dataclass diff --git a/src/zona/templates.py b/src/zona/templates.py index d5e0f22..4529143 100644 --- a/src/zona/templates.py +++ b/src/zona/templates.py @@ -27,7 +27,6 @@ def get_footer(template_dir: Path) -> str | None: return html_footer.read_text() -# TODO: add next/prev post button logic to posts # TODO: add a recent posts element that can be included elsewhere? class Templater: def __init__( @@ -79,11 +78,11 @@ class Templater: header=header, footer=footer, is_post=item.post, - next=util.normalize_url(item.next.url) - if item.next + newer=util.normalize_url(item.newer.url) + if item.newer else None, - previous=util.normalize_url(item.previous.url) - if item.previous + older=util.normalize_url(item.older.url) + if item.older else None, post_list=self.post_list, ) From c33acfa1c86e4ca37ab4ba7d87630b35014e53ce Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 17:52:49 -0400 Subject: [PATCH 04/19] added hover symbols to page titles --- src/zona/data/content/static/style.css | 109 ++++++++++++++++++++----- src/zona/data/templates/title.html | 2 +- 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/zona/data/content/static/style.css b/src/zona/data/content/static/style.css index f02717e..cd78efa 100644 --- a/src/zona/data/content/static/style.css +++ b/src/zona/data/content/static/style.css @@ -1,4 +1,5 @@ :root { + --main-placeholder-color: #b14242; --main-text-color: #b4b4b4; --main-text-opaque-color: rgba(180, 180, 180, 0.8); --main-bg-color: #121212; @@ -33,21 +34,65 @@ header { .post-nav { font-family: monospace; + font-size: 0.95em; + white-space: nowrap; +} + +.post-nav .bar { + position: relative; + bottom: 0.05em; + display: inline-block; + width: 1px; + height: 0.8em; + background-color: currentColor; + vertical-align: middle; + margin: 0 0.3em; +} + +.post-nav .placeholder { + color: var(--main-placeholder-color); } .post-nav .symbol { color: var(--main-bullet-color); + margin: 0; + padding: 0; + display: inline; } -.post-nav a { - margin: 0 2px; +.site-logo.hover-symbol::before { + content: "@"; } -.site-logo { +.title.hover-symbol::before { + content: ">"; + color: var(--main-bullet-color); +} + +.hover-symbol { color: inherit; + position: relative; font-weight: bold; text-decoration: none; - /* font-size: 1.75rem;*/ +} + +.hover-symbol::before { + font-family: monospace; + content: "#"; + position: absolute; + right: 100%; + margin-right: 0.25em; + top: 50%; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.2s ease; +} + +.hover-symbol:hover::before { + opacity: 1; +} +.hover-symbol:hover { + background-color: transparent; } .toclink { @@ -111,6 +156,11 @@ h1 { font-family: monospace; } +.title a { + color: inherit; + text-decoration: none; +} + article h1:first-of-type { margin-block-start: 1.67rem; } @@ -164,30 +214,45 @@ li::marker { /* Change this to your desired color */ } +/*a {*/ +/* color: var(--main-link-color);*/ +/* text-decoration: none;*/ +/* position: relative;*/ +/*}*/ + +/*a::after {*/ +/* content: "";*/ +/* position: absolute;*/ +/* left: 0;*/ +/* bottom: -2px;*/ +/* width: 100%;*/ +/* height: 1px;*/ +/* background-color: currentColor;*/ +/* transform: scaleX(0);*/ +/* transform-origin: center;*/ +/* transition: transform 0.1s ease;*/ +/*}*/ + +/*a:hover::after {*/ +/* transform: scaleX(1);*/ +/*}*/ +/*a:has(> code)::after {*/ +/* display: none;*/ +/*}*/ + a { color: var(--main-link-color); - text-decoration: none; - position: relative; + text-decoration: underline; + text-decoration-color: rgba(0, 0, 0, 0); + text-underline-offset: 2px; /* Optional: tweak spacing */ } -a::after { - content: ""; - position: absolute; - left: 0; - bottom: -2px; - width: 100%; - height: 1px; - background-color: currentColor; - transform: scaleX(0); - transform-origin: center; - transition: transform 0.1s ease; +a { + transition: text-decoration-color 0.15s ease; } -a:hover::after { - transform: scaleX(1); -} -a:has(> code)::after { - display: none; +a:hover { + text-decoration-color: currentColor; } max-width: 100%; diff --git a/src/zona/data/templates/title.html b/src/zona/data/templates/title.html index 9753365..1541095 100644 --- a/src/zona/data/templates/title.html +++ b/src/zona/data/templates/title.html @@ -1,2 +1,2 @@ -

{{ metadata.title }}

+

{{ metadata.title }}

From 4ea80a33f854d8a58f79abbefdcedd29f48ff635 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 18:35:13 -0400 Subject: [PATCH 05/19] improved styling of hover symbols --- src/zona/data/content/static/style.css | 83 +++++++------------------- 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/src/zona/data/content/static/style.css b/src/zona/data/content/static/style.css index cd78efa..56644c3 100644 --- a/src/zona/data/content/static/style.css +++ b/src/zona/data/content/static/style.css @@ -62,11 +62,12 @@ header { .site-logo.hover-symbol::before { content: "@"; + /* color: var(--main-bullet-color);*/ } .title.hover-symbol::before { content: ">"; - color: var(--main-bullet-color); + /* color: var(--main-bullet-color);*/ } .hover-symbol { @@ -74,6 +75,7 @@ header { position: relative; font-weight: bold; text-decoration: none; + transition: color 0.15s ease; } .hover-symbol::before { @@ -85,20 +87,24 @@ header { top: 50%; transform: translateY(-50%); opacity: 0; - transition: opacity 0.2s ease; + transition: opacity 0.15s ease, color 0.15s ease; + color: var(--main-text-color); } .hover-symbol:hover::before { opacity: 1; + color: var(--main-placeholder-color); /* only the symbol changes color */ } .hover-symbol:hover { background-color: transparent; + /* color: var(--main-placeholder-color);*/ } .toclink { position: relative; text-decoration: none; color: inherit; + transition: color 0.15s ease; } .toclink::before { @@ -109,7 +115,16 @@ header { top: 50%; transform: translateY(-50%); opacity: 0; - transition: opacity 0.2s ease; + transition: opacity 0.15s ease, color 0.15s ease; + color: var(--main-link-color); +} + +.toclink:hover::before { + opacity: 1; + color: var(--main-placeholder-color); +} +.toclink:hover { + background-color: transparent; } h1 .toclink::before { @@ -128,13 +143,6 @@ h4 .toclink::before { content: "###"; } -.toclink:hover::before { - opacity: 1; -} -.toclink:hover { - background-color: transparent; -} - /* h1, */ h2, h3, @@ -206,53 +214,26 @@ h6 { ul { list-style-type: disc; - /* or any other list style */ } li::marker { color: var(--main-bullet-color); - /* Change this to your desired color */ } -/*a {*/ -/* color: var(--main-link-color);*/ -/* text-decoration: none;*/ -/* position: relative;*/ -/*}*/ - -/*a::after {*/ -/* content: "";*/ -/* position: absolute;*/ -/* left: 0;*/ -/* bottom: -2px;*/ -/* width: 100%;*/ -/* height: 1px;*/ -/* background-color: currentColor;*/ -/* transform: scaleX(0);*/ -/* transform-origin: center;*/ -/* transition: transform 0.1s ease;*/ -/*}*/ - -/*a:hover::after {*/ -/* transform: scaleX(1);*/ -/*}*/ -/*a:has(> code)::after {*/ -/* display: none;*/ -/*}*/ - a { color: var(--main-link-color); text-decoration: underline; text-decoration-color: rgba(0, 0, 0, 0); - text-underline-offset: 2px; /* Optional: tweak spacing */ + text-underline-offset: 2px; } a { - transition: text-decoration-color 0.15s ease; + transition: color 0.15s ease, text-decoration-color 0.15s ease; } a:hover { - text-decoration-color: currentColor; + text-decoration-color: var(--main-placeholder-color); + color: var(--main-bullet-color); } max-width: 100%; @@ -489,23 +470,3 @@ caption { font-size: 0.8rem; color: var(--main-small-text-color); } - -a > code { - text-decoration: none; - color: var(--main-link-color); - position: relative; -} - -a:has(> code) { - text-decoration: none; - background: none; - /* position: static;*/ -} - -a:hover > code { - text-decoration: underline; -} - -a:hover:has(> code) { - background: none; -} From d35d3f61fbfa5aad4555a9d7dc83373ab8a71331 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 18:42:32 -0400 Subject: [PATCH 06/19] updated documentation --- README.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bd7eb9b..c3facfb 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,6 @@ If you don't want discovery, you can specify the project root as the first argument to `zona build`. You may specify a path for the output using the `--output/-o` flag. The `--draft/-d` flag includes draft posts in the output. -_Note: the previous build is _not_ cleaned before the new site is built. If -you've deleted some pages, you may need to remove the output directory before -rebuilding._ - ### Live Preview To make the writing process as frictionless as possible, zona ships with a live @@ -121,9 +117,10 @@ By default, the build outputs to a temporary directory. Use `-o/--output` to override this. **Note**: if the live preview isn't working as expected, try restarting the -server. If you change the configuration or any templates, the server must also -be restarted. The live preview uses the same function as `zona build` -internally; this means that the output is also written to disk. +server. If you change the configuration, the server must also be restarted. The +live preview uses the same function as `zona build` internally; this means that +the output is also written to disk --- a temporary directory by default, unless +overridden with `-o/--output`. #### Live Reload @@ -196,13 +193,17 @@ the packaged defaults. To apply a certain template to a page, set the `template` option in its [frontmatter](#frontmatter). The following public variables are made available to the template engine: -| Name | Description | -| ---------- | ------------------------------------------------------ | -| `content` | The content of this page. | -| `url` | The resolved URL of this page. | -| `metadata` | The frontmatter of this page (_merged with defaults_). | -| `header` | The sitemap header in HTML form. Can be `False`. | -| `footer` | The footer in HTML form. Can be `False`. | +| Name | Description | +| ----------- | -------------------------------------------------------- | +| `content` | The content of this page. | +| `url` | The resolved URL of this page. | +| `metadata` | The frontmatter of this page (_merged with defaults_). | +| `header` | The sitemap header in HTML form. Can be `False`. | +| `footer` | The footer in HTML form. Can be `False`. | +| `is_post` | Whether this page is a post. | +| `newer` | URL of the newer post in the post list. | +| `older` | URL of the older post in the post list. | +| `post_list` | A sorted list of `Item` objects. Meant for internal use. | #### Markdown Footer From a0fb62ac7abf7cf51dbf24a3b66d8c53e3d9d3f0 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 18:43:28 -0400 Subject: [PATCH 07/19] release 1.2.0 - Improved the appearance and semantics of post navigation buttons. - Navigation now follows "newer/older" logic. - Added hover symbols to page titles. - Improved the styling of hover symbols and links. --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e65c79..3790a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.2.0 + +- Improved the appearance and semantics of post navigation buttons. + - Navigation now follows "newer/older" logic. +- Added hover symbols to page titles. +- Improved the styling of hover symbols and links. + # 1.1.0 - Major improvements to default stylesheet. diff --git a/pyproject.toml b/pyproject.toml index 6b517bc..7653d22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zona" -version = "1.1.0" +version = "1.2.0" description = "Opinionated static site generator." license = "BSD-3-Clause " license-files = ["LICENSE"] diff --git a/uv.lock b/uv.lock index c1a3f41..da3457f 100644 --- a/uv.lock +++ b/uv.lock @@ -459,7 +459,7 @@ wheels = [ [[package]] name = "zona" -version = "1.1.0" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "dacite" }, From 8ad8bad438907c9c9f958d655904944fe0ffc53e Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 23:24:06 -0400 Subject: [PATCH 08/19] updated styling of lists --- src/zona/data/content/static/style.css | 65 +++++++++++++++++++++++--- src/zona/data/templates/post_list.html | 28 ++++++----- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/src/zona/data/content/static/style.css b/src/zona/data/content/static/style.css index 56644c3..caa0eef 100644 --- a/src/zona/data/content/static/style.css +++ b/src/zona/data/content/static/style.css @@ -61,13 +61,11 @@ header { } .site-logo.hover-symbol::before { - content: "@"; - /* color: var(--main-bullet-color);*/ + content: "~/"; } .title.hover-symbol::before { - content: ">"; - /* color: var(--main-bullet-color);*/ + content: "$"; } .hover-symbol { @@ -93,11 +91,28 @@ header { .hover-symbol:hover::before { opacity: 1; - color: var(--main-placeholder-color); /* only the symbol changes color */ + color: var(--main-placeholder-color); } .hover-symbol:hover { background-color: transparent; - /* color: var(--main-placeholder-color);*/ +} + +.toc ul { + font-family: monospace; + text-transform: lowercase; + margin: auto; + width: 50%; +} + +.toc ul ul { + padding-left: 1em; + margin-left: 1em; + /* list-style-type: "–– ";*/ +} +.toc ul ul ul { + padding-left: 1em; + margin-left: 1em; + /* list-style-type: "-- ";*/ } .toclink { @@ -105,6 +120,15 @@ header { text-decoration: none; color: inherit; transition: color 0.15s ease; + text-transform: lowercase; + font-family: monospace; +} +.post-list a { + position: relative; + text-decoration: none; + transition: color 0.15s ease; + text-transform: lowercase; + font-family: monospace; } .toclink::before { @@ -212,8 +236,35 @@ h6 { font-weight: bold; } +/*ul {*/ +/* list-style-type: disc;*/ +/*}*/ + ul { - list-style-type: disc; + list-style-type: "– "; +} +ul ul { + padding-left: 1em; + margin-left: 1em; + list-style-type: "+ "; +} +ul ul ul { + list-style-type: "~ "; +} +ul ul ul ul { + list-style-type: "• "; +} +ul ul ul ul ul { + list-style-type: "– "; +} +ul ul ul ul ul ul { + list-style-type: "+ "; +} +ul ul ul ul ul ul ul { + list-style-type: "~ "; +} +ul ul ul ul ul ul ul ul { + list-style-type: "• "; } li::marker { diff --git a/src/zona/data/templates/post_list.html b/src/zona/data/templates/post_list.html index 85e0be8..6b0bdca 100644 --- a/src/zona/data/templates/post_list.html +++ b/src/zona/data/templates/post_list.html @@ -1,20 +1,18 @@ -{% extends "base.html" %} {% block content %} - -{% if metadata.show_title %} -{% include "title.html" %} -{% endif %} +{% extends "base.html" %} {% block content %} {% if metadata.show_title %} {% +include "title.html" %} {% endif %}
{{ content | safe }}
{% if post_list %} - +
+ +
{% endif %} {% endblock %} - From 4cef77e4e0723896b834fa8fdb2e25bdbb09953f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Tue, 15 Jul 2025 23:39:00 -0400 Subject: [PATCH 09/19] ci: added test with cache --- .forgejo/workflows/test.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .forgejo/workflows/test.yml diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..3de0a8b --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,21 @@ +# this workflow checks if the project can be built successfully. +# it also serves to test whether based-alpine and uv-cache are working properly. +# Unit tests will be added here eventually +on: [push] +jobs: + test-build: + runs-on: based-alpine + steps: + - name: checkout source + uses: actions/checkout@v4 + - name: setup cache + id: uv-cache + uses: https://git.ficd.sh/ficd/uv-cache@v1 + - name: Check cache status + run: | + echo "Cache hit? ${{ steps.uv-cache.outputs.cache-hit }}" + - name: sync and build + run: | + uv sync + uv build + uv run zona --help From 3dc623f1c784077e237e78c4e4098b274fec6db8 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Wed, 16 Jul 2025 01:32:59 -0400 Subject: [PATCH 10/19] added --version flag to cli --- pyproject.toml | 2 +- src/zona/cli.py | 16 ++++++++++++++++ uv.lock | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7653d22..d852304 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zona" -version = "1.2.0" +version = "1.2.1" description = "Opinionated static site generator." license = "BSD-3-Clause " license-files = ["LICENSE"] diff --git a/src/zona/cli.py b/src/zona/cli.py index 4b48d3a..da15a22 100644 --- a/src/zona/cli.py +++ b/src/zona/cli.py @@ -1,3 +1,4 @@ +from importlib.metadata import version as __version__ from pathlib import Path from typing import Annotated @@ -134,8 +135,23 @@ def serve( ) +def version_callback(value: bool): + if value: + print(f"Zona version: {__version__('zona')}") + raise typer.Exit() + + @app.callback() def main_entry( + version: Annotated[ # pyright: ignore[reportUnusedParameter] + bool | None, + typer.Option( + "--version", + callback=version_callback, + is_eager=True, + help="Print version info and exit.", + ), + ] = None, verbosity: Annotated[ str, typer.Option( diff --git a/uv.lock b/uv.lock index da3457f..7ec1e2e 100644 --- a/uv.lock +++ b/uv.lock @@ -459,7 +459,7 @@ wheels = [ [[package]] name = "zona" -version = "1.2.0" +version = "1.2.1" source = { editable = "." } dependencies = [ { name = "dacite" }, From dea02e3a4e1250faa43e12b45a6f2cf14e24bbd0 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Wed, 16 Jul 2025 01:33:53 -0400 Subject: [PATCH 11/19] ci: updated test to check version --- .forgejo/workflows/test.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 3de0a8b..2a05ee6 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -11,11 +11,8 @@ jobs: - name: setup cache id: uv-cache uses: https://git.ficd.sh/ficd/uv-cache@v1 - - name: Check cache status - run: | - echo "Cache hit? ${{ steps.uv-cache.outputs.cache-hit }}" - name: sync and build run: | uv sync uv build - uv run zona --help + uv run zona --version From 59948ec51715bbb7a6227b141c795b9dd960fbd2 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Wed, 16 Jul 2025 01:35:19 -0400 Subject: [PATCH 12/19] updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3790a07..104815a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.2.1 + +- Added `--version` flag to CLI. + # 1.2.0 - Improved the appearance and semantics of post navigation buttons. From ac8c88e2af49eb427b43810d17af020b35fd4f29 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Wed, 16 Jul 2025 01:37:50 -0400 Subject: [PATCH 13/19] ci: updated publish job to use cache --- .forgejo/workflows/publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml index 53dd92f..fde3206 100644 --- a/.forgejo/workflows/publish.yml +++ b/.forgejo/workflows/publish.yml @@ -7,6 +7,9 @@ jobs: runs-on: based-alpine steps: - uses: actions/checkout@v4 + - name: setup cache + id: uv-cache + uses: https://git.ficd.sh/ficd/uv-cache@v1 - name: build run: | uv sync From 4f8979ae9bbb8a91ae4f37ff8eb8da3e2ef02659 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 17 Jul 2025 21:58:21 -0400 Subject: [PATCH 14/19] fixed incorrect codeberg links in readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c3facfb..ccf5dd1 100644 --- a/README.md +++ b/README.md @@ -231,11 +231,11 @@ modified if they point to a real file that's not included in the ignore list. Zona uses [Pygments] to provide syntax highlighting for fenced code blocks. The following Pygments plugins are included: -- [pygments-kakoune](https://codeberg.com/ficd/pygments-kakoune) +- [pygments-kakoune](https://codeberg.org/ficd/pygments-kakoune) - A lexer providing for highlighting Kakoune code. Available under the `kak` and `kakrc` aliases. -- [pygments-ashen](https://codeberg.com/ficd/ashen/tree/main/item/pygments/README.md) - - An implementation of the [Ashen](https://codeberg.com/ficd/ashen) theme for +- [pygments-ashen](https://codeberg.org/ficd/ashen/tree/main/item/pygments/README.md) + - An implementation of the [Ashen](https://codeberg.org/ficd/ashen) theme for Pygments. If you want to use any external Pygments styles or lexers, they must be @@ -442,7 +442,7 @@ output. If you set `draft: true` in a page's frontmatter, it will be marked as a draft. Drafts are completely excluded from `zona build` and `zona serve` unless the `--draft` flag is specified. -[Ashen]: https://codeberg.com/ficd/ashen +[Ashen]: https://codeberg.org/ficd/ashen [Pygments]: https://pygments.org/ ## Known Problems From 828a82e5cb62f59b82b9680a64a050d5d7c10f76 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 12 Jul 2025 01:06:31 -0400 Subject: [PATCH 15/19] feat: rss feed generation --- CHANGELOG.md | 6 +++ README.md | 63 ++++++++++++++++++++------ pyproject.toml | 3 +- src/zona/builder.py | 56 ++++++++++++++++++++++- src/zona/config.py | 51 ++++++++++++++++++++- src/zona/data/templates/page.html | 6 +-- src/zona/data/templates/post_list.html | 4 +- src/zona/layout.py | 7 ++- src/zona/metadata.py | 37 +++++++++++---- uv.lock | 52 +++++++++++++++++++++ 10 files changed, 251 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 104815a..269d2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.3.0 + +- Added RSS feed generation. +- Added default post description to configuration. +- Added time-of-day support to post `date` frontmatter parsing. + # 1.2.1 - Added `--version` flag to CLI. diff --git a/README.md b/README.md index ccf5dd1..6fe124a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ For an example of a website built with zona, please see - [Site Layout](#site-layout) - [Templates](#templates) - [Markdown Footer](#markdown-footer) + - [RSS Feed Generation](#rss-feed-generation) - [Internal Link Resolution](#internal-link-resolution) - [Syntax Highlighting](#syntax-highlighting) - [Markdown Extensions](#markdown-extensions) @@ -49,6 +50,7 @@ For an example of a website built with zona, please see - Live refresh in browser preview. - `jinja2` template support with sensible defaults included. - Basic page, blog post, post list. +- RSS feed generation. - Glob ignore. - YAML frontmatter. - Easily configurable sitemap header. @@ -212,6 +214,17 @@ it's parsed and rendered into HTML, then made available to other templates as the `footer` variable. If `footer.md` is missing but `footer.html` exists, then it's used instead. **Note: links are _not_ resolved in the footer.** +### RSS Feed Generation + +Zona can also generates an RSS feed containing your blog posts. This feature is +disabled by default, and you can enable it in the +[configuration](#configuration). + +The default location is a file called `rss.xml` in the content root. All RSS +related configuration is specified in `config.yml`. If you plan to use the feed, +make sure to replace the placeholder `link`, `title`, `description`, and +`author` configuration values before you set `enabled: true`. + ### Internal Link Resolution When zona encounters links in Markdown documents, it attempts to resolve them as @@ -309,18 +322,19 @@ YAML frontmatter can be used to configure the metadata of documents. All of them are optional. `none` is used when the option is unset. The following options are available: -| Key | Type & Default | Description | -| ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `title` | `str` = title-cased filename. | Title of the page. | -| `date` | Date string = file modified time. | Displayed on blog posts and used for post_list sorting. | -| `show_title` | `bool` = `true` | Whether `metadata.title` should be included in the template. | -| `header` | `bool` = `true` | Whether the header sitemap should be rendered. | -| `footer` | `bool` = `true` | Whether the footer should be rendered. | -| `template` | `str \| none` = `none` | Template to use for this page. Relative to `templates/`, `.html` extension optional. | -| `post` | `bool \| none` = `none` | Whether this page is a **post**. `true`/`false` is _absolute_. Leave it unset for automatic detection. | -| `draft` | `bool` = `false` | Whether this page is a draft. See [drafts](#drafts) for more. | -| `ignore` | `bool` = `false` | Whether this page should be ignored in _both_ `final` and `draft` contexts. | -| `math` | `bool` = `true` | Whether the LaTeX extension should be enabled for this page. | +| Key | Type & Default | Description | +| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `title` | `str` = title-cased filename. | Title of the page. | +| `description` | `str \| none` = `none` | Description. If omitted, default from [config](#configuration) will be used. | +| `date` | Date string = file modified time. | Displayed on blog posts and used for post_list sorting. | +| `show_title` | `bool` = `true` | Whether `metadata.title` should be included in the template. | +| `header` | `bool` = `true` | Whether the header sitemap should be rendered. | +| `footer` | `bool` = `true` | Whether the footer should be rendered. | +| `template` | `str \| none` = `none` | Template to use for this page. Relative to `templates/`, `.html` extension optional. | +| `post` | `bool \| none` = `none` | Whether this page is a **post**. `true`/`false` is _absolute_. Leave it unset for automatic detection. | +| `draft` | `bool` = `false` | Whether this page is a draft. See [drafts](#drafts) for more. | +| `ignore` | `bool` = `false` | Whether this page should be ignored in _both_ `final` and `draft` contexts. | +| `math` | `bool` = `true` | Whether the LaTeX extension should be enabled for this page. | **Note**: you can specify the date in any format that can be parsed by [`python-dateutil`](https://pypi.org/project/python-dateutil/). @@ -374,12 +388,23 @@ useful settings are listed here. Please see the default configuration: ```yaml -base_url: / +feed: + enabled: true + timezone: UTC + path: rss.xml + link: https://example.com + title: Zona Website + description: My zona website. + language: en + author: + name: John Doe + email: john@doe.net sitemap: Home: / ignore: - .marksman.toml markdown: + image_labels: true tab_length: 2 syntax_highlighting: enabled: true @@ -392,6 +417,8 @@ build: include_drafts: false blog: dir: blog + defaults: + description: A blog post server: reload: enabled: true @@ -400,6 +427,15 @@ server: | Name | Description | | -------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `feed.enabled` | Whether RSS feed should be generated. **Off by default**. | +| `feed.timezime` | Timezone to use for post `pubDate` values. Must be an IANA compliant string. | +| `feed.path` | Location of the feed, relative to content root. | +| `feed.link` | The base URL of the website. | +| `feed.title` | Website title. | +| `feed.description` | Website description. | +| `feed.language` | String specifying website's language code. | +| `author.name` | Your full name. | +| `author.email` | Your email address. | | `sitemap` | Sitemap dictionary. See [Sitemap](#sitemap). | | `ignore` | List of paths to ignore. See [Ignore List](#ignore-list). | | `markdown.tab_length` | How many spaces should be considered an indentation level. | @@ -410,6 +446,7 @@ server: | `build.clean_output_dir` | Whether previous build artifacts should be cleared when building. Recommended to leave this on. | | `build.include_drafts` | Whether drafts should be included by default. | | `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. | +| `blog.defaults.description` | Default description for blog posts with no `description` in their frontmatter. | | `server.reload.enabled` | Whether the preview server should use [live reload](#live-preview). | | `server.reload.scroll_tolerance` | The distance, in pixels, from the bottom to still count as "scrolled to bottom". | diff --git a/pyproject.toml b/pyproject.toml index d852304..b6d7de5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ authors = [ requires-python = ">=3.12" dependencies = [ "dacite>=1.9.2", + "feedgen>=1.0.0", "jinja2>=3.1.6", "l2m4m>=1.0.4", "markdown>=3.8.2", @@ -57,7 +58,7 @@ reportUnusedCallResult = false reportCallInDefaultInitializer = false enableTypeIgnoreComments = true reportIgnoreCommentWithoutRule = false -allowedUntypedLibraries = ["frontmatter", "pygments", "pymdownx", "l2m4m"] +allowedUntypedLibraries = ["frontmatter", "pygments", "pymdownx", "l2m4m", "feedgen", "feedgen.feed"] [tool.ruff] line-length = 70 diff --git a/src/zona/builder.py b/src/zona/builder.py index 416edc8..37a3bd1 100644 --- a/src/zona/builder.py +++ b/src/zona/builder.py @@ -2,6 +2,8 @@ import shutil from datetime import date from pathlib import Path +from feedgen.feed import FeedGenerator + from zona import markdown as zmd from zona import util from zona.config import ZonaConfig @@ -55,7 +57,9 @@ class ZonaBuilder: layout.root / "content" / "static" ): logger.debug(f"Parsing {path.name}.") - item.metadata, item.content = parse_metadata(path) + item.metadata, item.content = parse_metadata( + path, config=self.config + ) if item.metadata.ignore or ( item.metadata.draft and not self.config.build.include_drafts @@ -97,7 +101,45 @@ class ZonaBuilder: items.append(item) self.items = items - def _build(self): + def generate_feed(self) -> bytes: + post_list = self._get_post_list() + config = self.config.feed + if config.link.endswith("/"): + config.link = config.link[:-2] + fg = FeedGenerator() + fg.id(config.link) + fg.title(config.title) + author = { + "name": config.author.name, + "email": config.author.email, + } + fg.author(author) + fg.link( + href=f"{config.link}/{config.path}", + rel="self", + type="application/rss+xml", + ) + fg.language(config.language) + fg.description(config.description) + + for post in post_list: + assert post.metadata + fe = fg.add_entry() # pyright: ignore[reportUnknownVariableType] + fe.id(f"{config.link}{util.normalize_url(post.url)}") # pyright: ignore[reportUnknownMemberType] + fe.link( # pyright: ignore[reportUnknownMemberType] + href=f"{config.link}{util.normalize_url(post.url)}" + ) + fe.title(post.metadata.title) # pyright: ignore[reportUnknownMemberType] + fe.author(author) # pyright: ignore[reportUnknownMemberType] + desc = post.metadata.description + fe.description(desc) # pyright: ignore[reportUnknownMemberType] + date = post.metadata.date + fe.pubDate(date) # pyright: ignore[reportUnknownMemberType] + out: bytes = fg.rss_str(pretty=True) # pyright: ignore[reportUnknownVariableType] + assert isinstance(out, bytes) + return out + + def _get_post_list(self) -> list[Item]: assert self.items # sort according to date # descending order @@ -108,7 +150,12 @@ class ZonaBuilder: else date.min, reverse=True, ) + return post_list + + def _build(self): + post_list = self._get_post_list() # number of posts + # generate RSS here posts = len(post_list) # link post chronology for i, item in enumerate(post_list): @@ -180,4 +227,9 @@ class ZonaBuilder: self._discover() logger.debug("Building...") self._build() + if self.config.feed.enabled: + rss = self.generate_feed() + path = self.layout.content / self.config.feed.path + util.ensure_parents(path) + path.write_bytes(rss) self.fresh = False diff --git a/src/zona/config.py b/src/zona/config.py index 2a50fd4..84844c7 100644 --- a/src/zona/config.py +++ b/src/zona/config.py @@ -1,7 +1,13 @@ +from __future__ import annotations + from dataclasses import dataclass, field +from datetime import datetime, tzinfo from pathlib import Path +from typing import Any +from zoneinfo import ZoneInfo import yaml +from dacite import Config as DaciteConfig from dacite import from_dict from zona.log import get_logger @@ -25,9 +31,17 @@ def find_config(start: Path | None = None) -> Path | None: SitemapConfig = dict[str, str] +@dataclass +class PostDefaultsConfig: + description: str = "A blog post" + + @dataclass class BlogConfig: dir: str = "blog" + defaults: PostDefaultsConfig = field( + default_factory=PostDefaultsConfig + ) @dataclass @@ -69,12 +83,40 @@ class ServerConfig: reload: ReloadConfig = field(default_factory=ReloadConfig) +@dataclass +class AuthorConfig: + name: str = "John Doe" + email: str = "john@doe.net" + + +@dataclass +class FeedConfig: + enabled: bool = True + timezone: tzinfo = field(default_factory=lambda: ZoneInfo("UTC")) + path: str = "rss.xml" + link: str = "https://example.com" + title: str = "Zona Website" + description: str = "My zona website." + language: str = "en" + author: AuthorConfig = field(default_factory=AuthorConfig) + + IGNORELIST = [".marksman.toml"] +def parse_timezone(s: Any) -> tzinfo: + if isinstance(s, str): + return ZoneInfo(s) + else: + raise TypeError( + f"Expected {str}, got {type(s)} for config key timezone" + ) + + @dataclass class ZonaConfig: base_url: str = "/" + feed: FeedConfig = field(default_factory=FeedConfig) # dictionary where key is name, value is url sitemap: SitemapConfig = field( default_factory=lambda: {"Home": "/"} @@ -87,7 +129,12 @@ class ZonaConfig: server: ServerConfig = field(default_factory=ServerConfig) @classmethod - def from_file(cls, path: Path) -> "ZonaConfig": + def from_file(cls, path: Path) -> ZonaConfig: with open(path, "r") as f: raw = yaml.safe_load(f) - return from_dict(data_class=cls, data=raw) + config: ZonaConfig = from_dict( + data_class=cls, + data=raw, + config=DaciteConfig(type_hooks={tzinfo: parse_timezone}), + ) + return config diff --git a/src/zona/data/templates/page.html b/src/zona/data/templates/page.html index 0c22f8b..0ff499b 100644 --- a/src/zona/data/templates/page.html +++ b/src/zona/data/templates/page.html @@ -1,8 +1,8 @@ {% extends "base.html" %} {% block content %} {% if metadata.show_title %} {% -include "title.html" %} {% if metadata.date %} +include "title.html" %} {% if metadata.date.date() %}
- +
{% endif %} {% endif %} {% if is_post %} {% include "post_nav.html" %} {% endif %} diff --git a/src/zona/data/templates/post_list.html b/src/zona/data/templates/post_list.html index 6b0bdca..30ea74f 100644 --- a/src/zona/data/templates/post_list.html +++ b/src/zona/data/templates/post_list.html @@ -8,8 +8,8 @@ include "title.html" %} {% endif %}
    {% for item in post_list %}
  • - : {{ item.metadata.date.date() | safe}}: {{ item.metadata.title }}
  • {% endfor %} diff --git a/src/zona/layout.py b/src/zona/layout.py index 3d8de3d..e912dd0 100644 --- a/src/zona/layout.py +++ b/src/zona/layout.py @@ -1,5 +1,6 @@ from dataclasses import asdict, dataclass from pathlib import Path +from zoneinfo import ZoneInfo import typer import yaml @@ -145,9 +146,13 @@ def initialize_site(root: Path | None = None): logger.debug("Loading default configuation.") config = ZonaConfig() logger.debug(f"Writing default configuration to {config_path}.") + config_dict = asdict(config) + if "feed" in config_dict and "timezone" in config_dict["feed"]: + tz: ZoneInfo = config_dict["feed"]["timezone"] + config_dict["feed"]["timezone"] = tz.key with open(config_path, "w") as f: yaml.dump( - asdict(config), + config_dict, f, sort_keys=False, default_flow_style=False, diff --git a/src/zona/metadata.py b/src/zona/metadata.py index 98b14cf..4551117 100644 --- a/src/zona/metadata.py +++ b/src/zona/metadata.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from datetime import date +from datetime import date, datetime, time, tzinfo from pathlib import Path import frontmatter @@ -10,13 +10,14 @@ from dateutil import parser as date_parser from yaml import YAMLError import zona.util +from zona.config import ZonaConfig @dataclass class Metadata: title: str - date: date - description: str | None + date: datetime + description: str show_title: bool = True show_date: bool = True show_nav: bool = True @@ -30,14 +31,29 @@ class Metadata: math: bool = True -def parse_date(raw_date: str | date | object) -> date: - if isinstance(raw_date, date): - return raw_date +def ensure_timezone(dt: datetime, tz: tzinfo) -> datetime: + if dt.tzinfo is None or dt.utcoffset() is None: + dt = dt.replace(tzinfo=tz) + return dt + + +# TODO: migrate to using datetime, where user can optionall specify +# a time as well. if only date is given, default to time.min +def parse_date( + raw_date: str | datetime | date | object, tz: tzinfo +) -> datetime: + if isinstance(raw_date, datetime): + return ensure_timezone(raw_date, tz) + elif isinstance(raw_date, date): + return datetime.combine(raw_date, time.min, tzinfo=tz) assert isinstance(raw_date, str) - return date_parser.parse(raw_date).date() + dt = date_parser.parse(raw_date) + return ensure_timezone(dt, tz) -def parse_metadata(path: Path) -> tuple[Metadata, str]: +def parse_metadata( + path: Path, config: ZonaConfig +) -> tuple[Metadata, str]: """ Parses a file and returns parsed Metadata and its content. Defaults are applied for missing fields. If there is no metadata, a Metadata @@ -53,10 +69,11 @@ def parse_metadata(path: Path) -> tuple[Metadata, str]: raw_meta = post.metadata or {} defaults = { "title": zona.util.filename_to_title(path), - "date": date.fromtimestamp(path.stat().st_ctime), + "date": datetime.fromtimestamp(path.stat().st_ctime), + "description": config.blog.defaults.description, } meta = {**defaults, **raw_meta} - meta["date"] = parse_date(meta.get("date")) + meta["date"] = parse_date(meta.get("date"), config.feed.timezone) try: metadata = from_dict( data_class=Metadata, diff --git a/uv.lock b/uv.lock index 7ec1e2e..c81743b 100644 --- a/uv.lock +++ b/uv.lock @@ -44,6 +44,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" }, ] +[[package]] +name = "feedgen" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/59/be0a6f852b5dfbf19e6c8e962c8f41407697f9f52a7902250ed98683ae89/feedgen-1.0.0.tar.gz", hash = "sha256:d9bd51c3b5e956a2a52998c3708c4d2c729f2fcc311188e1e5d3b9726393546a", size = 258496, upload-time = "2023-12-25T18:04:08.421Z" } + [[package]] name = "iniconfig" version = "2.1.0" @@ -87,6 +97,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/fd/aba08bb9e527168efad57985d7db9a853eb2384b1efa5ca5f3a3794c9cef/latex2mathml-3.78.0-py3-none-any.whl", hash = "sha256:1aeca3dc027b3006ad7b301b7f4a15ffbb4c1451e3dc8c3389e97b37b497e1d6", size = 73673, upload-time = "2025-05-03T16:51:51.991Z" }, ] +[[package]] +name = "lxml" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, + { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, + { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, + { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" }, + { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" }, + { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" }, + { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" }, + { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" }, + { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, +] + [[package]] name = "markdown" version = "3.8.2" @@ -463,6 +513,7 @@ version = "1.2.1" source = { editable = "." } dependencies = [ { name = "dacite" }, + { name = "feedgen" }, { name = "jinja2" }, { name = "l2m4m" }, { name = "markdown" }, @@ -489,6 +540,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "dacite", specifier = ">=1.9.2" }, + { name = "feedgen", specifier = ">=1.0.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "l2m4m", specifier = ">=1.0.4" }, { name = "markdown", specifier = ">=3.8.2" }, From d8d1e991c2517ef3180856279df80f8779433e3f Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Fri, 18 Jul 2025 00:23:51 -0400 Subject: [PATCH 16/19] feat: init now only includes footer template --- src/zona/layout.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/zona/layout.py b/src/zona/layout.py index e912dd0..f4c7311 100644 --- a/src/zona/layout.py +++ b/src/zona/layout.py @@ -125,7 +125,8 @@ def initialize_site(root: Path | None = None): layout = Layout.from_input(root=root, validate=False) # load template resources logger.debug("Loading internal templates.") - templates = util.get_resources("templates") + # only write the footer + templates = [util.get_resource("templates/footer.md")] logger.debug("Loading internal static content.") static = util.get_resources("content") for dir, resources in [ From 312818d8a6e098494e2f5254cfbd1240b2b7cd84 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Fri, 18 Jul 2025 00:38:32 -0400 Subject: [PATCH 17/19] doc: updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 269d2ab..9f6eed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Added RSS feed generation. - Added default post description to configuration. - Added time-of-day support to post `date` frontmatter parsing. +- `zona init` now only writes `footer.md` to the templates directory. # 1.2.1 From 182b30a4efcd4e9c9898a84b511bedf904afad00 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Fri, 18 Jul 2025 00:41:04 -0400 Subject: [PATCH 18/19] fix: rss feed is no longer written to content directory --- src/zona/builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zona/builder.py b/src/zona/builder.py index 37a3bd1..dd9509f 100644 --- a/src/zona/builder.py +++ b/src/zona/builder.py @@ -229,7 +229,7 @@ class ZonaBuilder: self._build() if self.config.feed.enabled: rss = self.generate_feed() - path = self.layout.content / self.config.feed.path + path = self.layout.output / self.config.feed.path util.ensure_parents(path) path.write_bytes(rss) self.fresh = False From 66a3eb7f8c89242a393e188549b415b0a5764586 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Fri, 18 Jul 2025 00:42:45 -0400 Subject: [PATCH 19/19] release: 1.2.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b6d7de5..185c12d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zona" -version = "1.2.1" +version = "1.2.2" description = "Opinionated static site generator." license = "BSD-3-Clause " license-files = ["LICENSE"]