added config option for preview scroll tolerance

This commit is contained in:
Daniel Fichtinger 2025-07-13 17:31:25 -04:00
parent fe0f338803
commit 10d1772a2d
5 changed files with 217 additions and 175 deletions

328
README.md
View file

@ -1,24 +1,22 @@
<h1>zona</h1>
[zona](https://sr.ht/~ficd/zona) is an _opinionated_ static site generator
written in Python. From a structured directory of Markdown content, zona
builds a simple static website. It's designed to get out of your way and
let you focus on writing.
[zona](https://git.ficd.sh/ficd/zona) is an _opinionated_ static site generator
written in Python. From a structured directory of Markdown content, zona builds
a simple static website. It's designed to get out of your way and let you focus
on writing.
**What do I mean by opinionated?** I built zona primarily for myself. I've
tried making it flexible by exposing as many variables as possible to the
template engine. However, if you're looking for something stable,
complete, and fully configurable, zona may not be for you. If you want a
minimal Markdown blog and are comfortable with modifying `jinja2`
templates and CSS, then you're in luck.
**What do I mean by opinionated?** I built zona primarily for myself. I've tried
making it flexible by exposing as many variables as possible to the template
engine. However, if you're looking for something stable, complete, and fully
configurable, zona may not be for you. If you want a minimal Markdown blog and
are comfortable with modifying `jinja2` templates and CSS, then you're in luck.
**Note:** This project is in early development, there are no versioned
releases yet, and breaking changes are likely. Versioned releases will be
made and zona will be published to PyPI once it's stable. zona was
previously implemented in Go; I decided to rewrite the project in Python.
If you're interested in seeing the previous codebase (which is feature
incomplete), visit the [~ficd/zona-go](https://git.sr.ht/~ficd/zona-go)
repository.
**Note:** This project is in early development, there are no versioned releases
yet, and breaking changes are likely. Versioned releases will be made and zona
will be published to PyPI once it's stable. zona was previously implemented in
Go; I decided to rewrite the project in Python. If you're interested in seeing
the previous codebase (which is feature incomplete), visit the
[zona-go](https://git.ficd.sh/ficd/zona-go) repository.
For an example of a website built with zona, please see
[ficd.sh](https://ficd.sh).
@ -31,6 +29,7 @@ For an example of a website built with zona, please see
- [Getting Started](#getting-started)
- [Building](#building)
- [Live Preview](#live-preview)
- [Live Reload](#live-reload)
- [How It Works](#how-it-works)
- [Site Layout](#site-layout)
- [Templates](#templates)
@ -60,15 +59,15 @@ For an example of a website built with zona, please see
- Easily configurable sitemap header.
- Site footer written in Markdown.
- Smart site layout discovery.
- Blog posts are automatically discovered and rendered accordingly (can
be overridden in frontmatter).
- Blog posts are automatically discovered and rendered accordingly (can be
overridden in frontmatter).
- Extended Markdown renderer:
- Smart internal link resolution.
- Syntax highlighting.
- Includes Kakoune syntax and [Ashen] highlighting.
- [Image labels](#image-labels).
- Many `python-markdown` extensions enabled, including footnotes,
tables, abbreviations, etc.
- Many `python-markdown` extensions enabled, including footnotes, tables,
abbreviations, etc.
- LaTeX support.
## Installation
@ -77,7 +76,7 @@ zona is not yet packaged on PyPI. You may use `uv` to install it from this
repository:
```sh
uv tool install 'git+https://git.sr.ht/~ficd/zona'
uv tool install 'git+https://git.ficd.sh/ficd/zona'
```
## Usage
@ -87,71 +86,78 @@ available options and arguments._
### Getting Started
To set up a new website, create a new directory and run `zona init` inside
of it. This creates the required directory structure and writes the
default configuration file. The default templates and default stylesheet
are also written.
To set up a new website, create a new directory and run `zona init` inside of
it. This creates the required directory structure and writes the default
configuration file. The default templates and default stylesheet are also
written.
### Building
To build the website, run `zona build`. The project root is discovered
according to the location of `config.yml`. By default, the output
directory is called `public`, and saved inside the root directory.
To build the website, run `zona build`. The project root is discovered according
to the location of `config.yml`. By default, the output directory is called
`public`, and saved inside the root directory.
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.
`--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._
_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 preview server. It spins up an HTTP server, meaning that internal
links work properly (this is not the case if you simply open the `.html`
files in your browser.)
To make the writing process as frictionless as possible, zona ships with a live
preview server. It spins up an HTTP server, meaning that internal links work
properly (this is not the case if you simply open the `.html` files in your
browser.)
Additionally, the server watches for changes to all source files, and
rebuilds the website when they're modified. _Note: the entire website is
rebuilt — this ensures that links are properly resolved._
Additionally, the server watches for changes to all source files, and rebuilds
the website when they're modified. _Note: the entire website is rebuilt — this
ensures that links are properly resolved._
Optionally, live reloading of the browser is also provided. With this
feature (enabled by default), your browser will automatically refresh open
pages whenever the site is rebuilt. The live reloading requires JavaScript
support from the browser — this is why the feature is optional.
Drafts are enabled by default in live preview. Use `--final/-f` to disable them.
By default, the build outputs to a temporary directory. Use `-o/--output` to
override this.
To start a preview server, use `zona serve`. You can specify the root
directory as its first argument. Use the `--host` to specify a host name
(`localhost` by default) and `--port/-p` to specify a port (default:
`8000`). The `--no-live-reload/-n` disables the live browser reloading
(_automatic site rebuilds are not disabled_).
**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.
Drafts are enabled by default in live preview. Use `--final/-f` to disable
them. By default, the build outputs to a temporary directory. Use
`-o/--output` to override this.
#### Live Reload
**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.
Optionally, live reloading of the browser is also provided. With this feature
(enabled by default), your browser will automatically refresh open pages
whenever the site is rebuilt. The live reloading requires JavaScript support
from the browser — this is why the feature is optional.
To start a preview server, use `zona serve`. You can specify the root directory
as its first argument. Use the `--host` to specify a host name (`localhost` by
default) and `--port/-p` to specify a port (default: `8000`).
The `--live-reload/--no-live-reload` option overrides the value set in the
[config](#configuration) (`true` by default). _Automatic site rebuilds are not
affected_.
If you are scrolled to the bottom of the page in the browser, and you extend the
height of the page by adding new content, you will automatically be scrolled to
the _new_ bottom after reloading. You may tune the tolerance threshold in the
[configuration](#configuration).
#### How It Works
The basic idea is this: after a rebuild, the server needs to notify your
browser to refresh the open pages. We implement this using a small amount
of JavaScript. The server injects a tiny script into any HTML page it
serves; which causes your browser to open a WebSocket connection with the
server. When the site is rebuilt, the server notifies your browser via the
WebSocket, which reloads the page.
The basic idea is this: after a rebuild, the server needs to notify your browser
to refresh the open pages. We implement this using a small amount of JavaScript.
The server injects a tiny script into any HTML page it serves; which causes your
browser to open a WebSocket connection with the server. When the site is
rebuilt, the server notifies your browser via the WebSocket, which reloads the
page.
Unfortunately, there is no way to implement this feature without using
JavaScript. **JavaScript is _only_ used for the live preview feature. The
script is injected by the server, and never written to the HTML files in
the output directory.**
JavaScript. **JavaScript is _only_ used for the live preview feature. The script
is injected by the server, and never written to the HTML files in the output
directory.**
### Site Layout
@ -164,32 +170,30 @@ templates/
public/
```
The **root** of the zona **project** _must_ contain the configuration
file, `config.yml`, and a directory called `content`. A directory called
`templates` is optional, and prioritized if it exists. `public` is the
built site output — it's recommended to add this path to your
`.gitignore`.
The **root** of the zona **project** _must_ contain the configuration file,
`config.yml`, and a directory called `content`. A directory called `templates`
is optional, and prioritized if it exists. `public` is the built site output —
it's recommended to add this path to your `.gitignore`.
The `content` directory is the **root of the website**. Think of it as the
**content root**. For example, suppose your website is hosted at
`example.com`. `content/blog/index.md` corresponds to `example.com/blog`,
**content root**. For example, suppose your website is hosted at `example.com`.
`content/blog/index.md` corresponds to `example.com/blog`,
`content/blog/my-post.md` becomes `example.com/blog/my-post`, etc.
- Internal links are resolved **relative to the `content` directory.**
- Templates are resolved relative to the `template` directory.
Markdown files inside a certain directory (`content/blog` by default) are
automatically treated as _blog posts_. This means they are rendered with
the `page` template, and included in the `post_list`, which can be
included in your site using the `post_list` template.
automatically treated as _blog posts_. This means they are rendered with the
`page` template, and included in the `post_list`, which can be included in your
site using the `post_list` template.
### Templates
The `templates` directory may contain any `jinja2` template files. You may
modify the existing templates or create your own. 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:
modify the existing templates or create your own. 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 |
| ---------- | ------------------------------------------------------ |
@ -201,43 +205,40 @@ available to the template engine:
#### Markdown Footer
The `templates` directory can contain a file called `footer.md`. If it
exists, 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.**
The `templates` directory can contain a file called `footer.md`. If it exists,
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.**
### Internal Link Resolution
When zona encounters links in Markdown documents, it attempts to resolve
them as internal links. Links beginning with `/` are resolved relative to
the content root; otherwise, they are resolved relative to the Markdown
file. If the link resolves to an existing file that is part of the
website, it's replaced with an appropriate web-server-friendly link.
Otherwise, the link isn't changed.
When zona encounters links in Markdown documents, it attempts to resolve them as
internal links. Links beginning with `/` are resolved relative to the content
root; otherwise, they are resolved relative to the Markdown file. If the link
resolves to an existing file that is part of the website, it's replaced with an
appropriate web-server-friendly link. Otherwise, the link isn't changed.
For example, suppose the file `blog/post1.md` has a link `./post2.md`. The
HTML output will contain the link `/blog/post2` (which corresponds to
`/blog/post2/index.html`). Link resolution is applied to _all_ internal
links, including those pointing to static resources like images. Links are
only modified if they point to a real file that's not included in the
ignore list.
For example, suppose the file `blog/post1.md` has a link `./post2.md`. The HTML
output will contain the link `/blog/post2` (which corresponds to
`/blog/post2/index.html`). Link resolution is applied to _all_ internal links,
including those pointing to static resources like images. Links are only
modified if they point to a real file that's not included in the ignore list.
### Syntax Highlighting
Zona uses [Pygments] to provide syntax highlighting for fenced code
blocks. The following Pygments plugins are included:
Zona uses [Pygments] to provide syntax highlighting for fenced code blocks. The
following Pygments plugins are included:
- [pygments-kakoune](https://git.sr.ht/~ficd/pygments-kakoune)
- A lexer providing for highlighting Kakoune code. Available under the
`kak` and `kakrc` aliases.
- [pygments-ashen](https://git.sr.ht/~ficd/ashen/tree/main/item/pygments/README.md)
- An implementation of the [Ashen](https://git.sr.ht/~ficd/ashen) theme
for Pygments.
- [pygments-kakoune](https://codeberg.com/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.
If you want to use any external Pygments styles or lexers, they must be
available in zona's Python environment. For example, you can give zona
access to [Catppucin](https://github.com/catppuccin/python):
available in zona's Python environment. For example, you can give zona access to
[Catppucin](https://github.com/catppuccin/python):
```yaml
# config.yml
@ -252,9 +253,9 @@ Then, run zona with the following `uv` command:
uvx --with catppucin zona build
```
Inline syntax highlighting is also provided via a `python-markdown`
extension. If you prefix inline code with a shebang followed by the
language identifier, it will be highlighted. For example:
Inline syntax highlighting is also provided via a `python-markdown` extension.
If you prefix inline code with a shebang followed by the language identifier, it
will be highlighted. For example:
```
`#!python print(f"I love {foobar}!", end="")`
@ -280,10 +281,9 @@ will be rendered as
### Image Labels
A feature unique to zona is **image labels**. They make it easy to
annotate images in your Markdown documents. The alt text Markdown element
is rendered as the label — with support for inline Markdown. Consider this
example:
A feature unique to zona is **image labels**. They make it easy to annotate
images in your Markdown documents. The alt text Markdown element is rendered as
the label — with support for inline Markdown. Consider this example:
```markdown
![This **image** has _markup_.](static/markdown.png)
@ -298,14 +298,14 @@ The above results in the following HTML:
```
The `image-container` class is provided as a convenience for styling. The
default stylesheet centers the label under the image. Note: _links_ inside
image captions are not currently supported. I am looking into a solution.
default stylesheet centers the label under the image. Note: _links_ inside image
captions are not currently supported. I am looking into a solution.
### Frontmatter
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:
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 |
| ------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------ |
@ -338,10 +338,9 @@ template: post_list
Welcome to my blog! Please find a list of my posts below.
```
Setting `post: false` is necessary because, by default, all documents
inside `content/blog` are considered to be posts unless explicitly
disabled in the frontmatter. We don't want the post list to list _itself_
as a post.
Setting `post: false` is necessary because, by default, all documents inside
`content/blog` are considered to be posts unless explicitly disabled in the
frontmatter. We don't want the post list to list _itself_ as a post.
Then, you'd create `content/blog/my-post.md` and populate it:
@ -352,32 +351,32 @@ date: July 5, 2025
---
```
Because `my-post` is inside the `blog` directory, `post: true` is implied.
If you wanted to put it somewhere outside `blog`, you would need to set
Because `my-post` is inside the `blog` directory, `post: true` is implied. If
you wanted to put it somewhere outside `blog`, you would need to set
`post: true` for it to be included in the post list.
## Configuration
Zona is configured in YAML format. The configuration file is called
`config.yml` and it **must** be located in the root of the project — in
the same directory as `content` and `templates`.
Zona is configured in YAML format. The configuration file is called `config.yml`
and it **must** be located in the root of the project — in the same directory as
`content` and `templates`.
Your configuration will be merged with the defaults. `zona init` also
writes a copy of the default configuration to the correct location. If it
exists, you'll be prompted before overwriting it.
Your configuration will be merged with the defaults. `zona init` also writes a
copy of the default configuration to the correct location. If it exists, you'll
be prompted before overwriting it.
**Note:** Currently, not every configuration value is actually used. Only
the useful settings are listed here.
**Note:** Currently, not every configuration value is actually used. Only the
useful settings are listed here.
Please see the default configuration:
```yaml
base_url: /
sitemap:
Home: /
ignore:
- .marksman.toml
markdown:
image_labels: true
tab_length: 2
syntax_highlighting:
enabled: true
@ -385,49 +384,60 @@ markdown:
wrap: false
links:
external_new_tab: true
build:
clean_output_dir: true
include_drafts: false
blog:
dir: blog
server:
reload:
enabled: true
scroll_tolerance: 100
```
| Name | Description |
| -------------------------------------- | --------------------------------------------------------------------------------------------- |
| `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. |
| `markdown.syntax_highlighting.enabled` | Whether code should be highlighted. |
| `markdown.syntax_highlighting.theme` | [Pygments] style for highlighting. |
| `markdown.syntax_highlighting.wrap` | Whether the resulting code block should be word wrapped. |
| `markdown.links.external_new_tab` | Whether external links should be opened in a new tab. |
| `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. |
| Name | Description |
| -------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `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. |
| `markdown.syntax_highlighting.enabled` | Whether code should be highlighted. |
| `markdown.syntax_highlighting.theme` | [Pygments] style for highlighting. |
| `markdown.syntax_highlighting.wrap` | Whether the resulting code block should be word wrapped. |
| `markdown.links.external_new_tab` | Whether external links should be opened in a new tab. |
| `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. |
| `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". |
### Sitemap
You can define a sitemap in the configuration file. This is a list of
links that will be rendered at the top of every page. The `sitemap` is a
dictionary of `string` to `string` pairs, where each key is the displayed
text of the link, and the value if the `href`. Consider this example:
You can define a sitemap in the configuration file. This is a list of links that
will be rendered at the top of every page. The `sitemap` is a dictionary of
`string` to `string` pairs, where each key is the displayed text of the link,
and the value if the `href`. Consider this example:
```yaml
sitemap:
Home: /
About: /about
Blog: /blog
Git: https://git.sr.ht/~ficd
Git: https://git.ficd.sh/ficd
```
### Ignore List
You can set a list of glob patterns in the [configuration](#configuration)
that should be ignored by zona. This is useful because zona makes a copy
of _every_ file it encounters inside the `content` directory, regardless
of its type. The paths must be relative to the `content` directory.
You can set a list of glob patterns in the [configuration](#configuration) that
should be ignored by zona. This is useful because zona makes a copy of _every_
file it encounters inside the `content` directory, regardless of its type. The
paths must be relative to the `content` directory.
### Drafts
zona allows you to begin writing content without including it in the final
build 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.
zona allows you to begin writing content without including it in the final build
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://sr.ht/~ficd/ashen
[Ashen]: https://codeberg.com/ficd/ashen
[Pygments]: https://pygments.org/