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
diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml
new file mode 100644
index 0000000..2a05ee6
--- /dev/null
+++ b/.forgejo/workflows/test.yml
@@ -0,0 +1,18 @@
+# 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: sync and build
+ run: |
+ uv sync
+ uv build
+ uv run zona --version
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}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e65c79..104815a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# 1.2.1
+
+- Added `--version` flag to CLI.
+
+# 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/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
diff --git a/pyproject.toml b/pyproject.toml
index 6b517bc..f0c6b14 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "zona"
-version = "1.1.0"
+version = "1.2.1"
description = "Opinionated static site generator."
license = "BSD-3-Clause "
license-files = ["LICENSE"]
@@ -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"]
[tool.ruff]
line-length = 70
diff --git a/src/zona/builder.py b/src/zona/builder.py
index 5c8af6b..bd2812b 100644
--- a/src/zona/builder.py
+++ b/src/zona/builder.py
@@ -1,7 +1,9 @@
import shutil
-from datetime import date
+from datetime import date, datetime
from pathlib import Path
+from feedgen.feed import FeedGenerator
+
from zona import markdown as zmd
from zona import util
from zona.config import ZonaConfig
@@ -49,9 +51,7 @@ class ZonaBuilder:
destination=destination,
url=str(destination.relative_to(layout.output)),
)
- if path.name.endswith(
- ".md"
- ) and not path.is_relative_to(
+ if path.name.endswith(".md") and not path.is_relative_to(
layout.root / "content" / "static"
):
logger.debug(f"Parsing {path.name}.")
@@ -72,13 +72,11 @@ class ZonaBuilder:
item.copy = False
name = destination.stem
if name == "index":
- item.destination = (
- item.destination.with_suffix(".html")
+ item.destination = item.destination.with_suffix(
+ ".html"
)
else:
- relative = path.relative_to(base).with_suffix(
- ""
- )
+ relative = path.relative_to(base).with_suffix("")
name = relative.stem
item.destination = (
layout.output
@@ -90,15 +88,43 @@ class ZonaBuilder:
layout.output
)
item.url = (
- ""
- if rel_url == Path(".")
- else rel_url.as_posix()
+ "" if rel_url == Path(".") else rel_url.as_posix()
)
items.append(item)
self.items = items
- def _build(self):
+ def generate_feed(self) -> str:
+ post_list = self._get_post_list()
+ config = self.config.feed
+ 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=config.link, rel="self")
+ 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.title(post.metadata.title) # pyright: ignore[reportUnknownMemberType]
+ fe.author(author) # pyright: ignore[reportUnknownMemberType]
+ fe.description(post.metadata.description) # pyright: ignore[reportUnknownMemberType]
+ date = post.metadata.date
+ # ISSUE: lack of timezone causes an error, need to add TZ config option
+ dt = datetime.combine(date, datetime.min.time())
+ fe.pubDate(dt) # pyright: ignore[reportUnknownMemberType]
+ return fg.rss_str(pretty=True) # pyright: ignore[reportUnknownVariableType]
+
+ def _get_post_list(self) -> list[Item]:
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 +132,17 @@ class ZonaBuilder:
else date.min,
reverse=True,
)
+ # number of posts
+ # generate RSS here
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,
@@ -125,9 +156,7 @@ class ZonaBuilder:
# write code highlighting stylesheet
if self.config.markdown.syntax_highlighting.enabled:
pygments_style = zmd.get_style_defs(self.config)
- pygments_path = (
- self.layout.output / "static" / "pygments.css"
- )
+ pygments_path = self.layout.output / "static" / "pygments.css"
util.ensure_parents(pygments_path)
pygments_path.write_text(pygments_style)
for item in self.item_map.values():
@@ -175,3 +204,4 @@ class ZonaBuilder:
logger.debug("Building...")
self._build()
self.fresh = False
+ print(self.generate_feed())
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/src/zona/config.py b/src/zona/config.py
index 2a50fd4..59c14b2 100644
--- a/src/zona/config.py
+++ b/src/zona/config.py
@@ -69,16 +69,32 @@ 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
+ path: str = "feed.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"]
@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": "/"}
- )
+ sitemap: SitemapConfig = field(default_factory=lambda: {"Home": "/"})
# list of globs relative to content that should be ignored
ignore: list[str] = field(default_factory=lambda: IGNORELIST)
markdown: MarkdownConfig = field(default_factory=MarkdownConfig)
diff --git a/src/zona/data/content/static/style.css b/src/zona/data/content/static/style.css
index f02717e..caa0eef 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,27 +34,101 @@ 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: "$";
+}
+
+.hover-symbol {
color: inherit;
+ position: relative;
font-weight: bold;
text-decoration: none;
- /* font-size: 1.75rem;*/
+ transition: color 0.15s ease;
+}
+
+.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.15s ease, color 0.15s ease;
+ color: var(--main-text-color);
+}
+
+.hover-symbol:hover::before {
+ opacity: 1;
+ color: var(--main-placeholder-color);
+}
+.hover-symbol:hover {
+ background-color: transparent;
+}
+
+.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 {
position: relative;
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 {
@@ -64,7 +139,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 {
@@ -83,13 +167,6 @@ h4 .toclink::before {
content: "###";
}
-.toclink:hover::before {
- opacity: 1;
-}
-.toclink:hover {
- background-color: transparent;
-}
-
/* h1, */
h2,
h3,
@@ -111,6 +188,11 @@ h1 {
font-family: monospace;
}
+.title a {
+ color: inherit;
+ text-decoration: none;
+}
+
article h1:first-of-type {
margin-block-start: 1.67rem;
}
@@ -154,40 +236,55 @@ h6 {
font-weight: bold;
}
+/*ul {*/
+/* list-style-type: disc;*/
+/*}*/
+
ul {
- list-style-type: disc;
- /* or any other list style */
+ 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 {
color: var(--main-bullet-color);
- /* Change this to your desired color */
}
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;
}
-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: color 0.15s ease, text-decoration-color 0.15s ease;
}
-a:hover::after {
- transform: scaleX(1);
-}
-a:has(> code)::after {
- display: none;
+a:hover {
+ text-decoration-color: var(--main-placeholder-color);
+ color: var(--main-bullet-color);
}
max-width: 100%;
@@ -424,23 +521,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;
-}
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 %}