Introduction
Welcome to Taxus, a Rust-based static site generator built with Tera, featuring optional WebAssembly "islands" for interactive components.
What is Taxus?
Taxus is a static site generator that combines:
- Tera templates for the static "sea of HTML" — page layout, content, navigation
- Markdown content with TOML frontmatter for writing pages and posts
- SCSS for modern styling
- Optional Yew components — pre-rendered server-side at build time, hydrated by WASM in the browser for interactivity
The key insight: pages are immediately visible with no JavaScript required. Interactive components load asynchronously and attach without re-rendering.
Features
- Static-first: Pre-rendered HTML for optimal performance and SEO
- Islands Architecture: Interactive Yew WASM components embedded within static pages
- Markdown Content: Write content in Markdown files with TOML frontmatter
- Co-located Assets: Images and files in the content directory are automatically copied to output
- Internal Links: Reference other pages by content file path with build-time validation
- Blog Features: Summary extraction, reading time, word count, custom slugs
- Pagination: Split large collections across multiple pages
- RSS/Atom Feeds: Automatic feed generation for content syndication
- Sitemap: Automatic
sitemap.xmlgeneration with priorities and lastmod dates - Taxonomies: Tags, categories, and series for content organization
- Development Server: Hot-reloading local server with WebSocket live reload
- CLI Interface:
build,clean,init,routes, andservesubcommands
Who is this for?
Taxus is ideal for:
- Rust developers who want to build websites without leaving their favorite language
- Performance enthusiasts who want fast, optimized static sites with selective WASM interactivity
- SEO-conscious developers who need pre-rendered content with no JavaScript dependency for initial render
- Component lovers who prefer the Yew component model for interactive UI pieces
Documentation Overview
- Getting Started — Quick start guide
- Architecture — Technical overview of the workspace, modules, and build pipeline
- Configuration —
site.tomlformat and options - Content — Markdown files, frontmatter, taxonomies, pagination
- Templates — Tera templates and context variables
- Islands Architecture — How to write and use Yew components
- CLI Reference — Command-line interface documentation
- Development Server — Hot reload and file watching
- API Reference — Library API documentation
License
This project is licensed under the MIT License - see the License.txt file for details.
Getting Started
This guide will help you get up and running with Taxus quickly.
Prerequisites
Before you begin, ensure you have the following installed:
- Rust (edition 2024) — Install Rust
Quick Start
Step 1: Clone and Initialize
# Clone the repository
git clone https://github.com/crustyrustacean/taxus.git
cd taxus
# Create a new site
cargo run -- init my-site --name "My Site" --base-url "https://example.com"
This creates the following structure:
my-site/
├── site.toml # Site configuration
├── content/
│ └── _index.md # Home page
├── templates/
│ ├── base.html # Base HTML layout
│ ├── page.html # Single-page template
│ ├── section.html # Section/listing template
│ ├── tags.html # Tag listing page
│ ├── tags_term.html # Individual tag page
│ ├── categories.html # Category listing page
│ ├── categories_term.html # Individual category page
│ ├── series.html # Series listing page
│ ├── series_term.html # Individual series page
│ └── 404.html # Not found page
├── static/
│ ├── scripts.js # Placeholder scripts
│ └── favicon.png # Placeholder favicon
└── styles/
└── main.scss # Starter stylesheet
Step 2: Build the Site
cargo run -- build --dir my-site --verbose
This runs the 13-stage build pipeline and writes output to my-site/dist/.
Step 3: Serve and View
cargo run -- serve --dir my-site --open
This starts a development server at http://localhost:3000 and opens it in your browser.
You should see the home page rendered from the Markdown content in content/_index.md.
Next Steps
- Learn about Configuration for customizing your site
- Understand Content for writing pages and posts
- Explore Templates for customizing HTML output
- Read the CLI Reference for all command options
For Islands Support
If you want interactive Yew components:
# Initialize with islands support
cargo run -- init my-site --islands
# Build with islands feature enabled
# The WASM client is compiled automatically and embedded in the binary
cargo run --features islands -- build --dir my-site
The WASM client (client.js and client_bg.wasm) is compiled during the Cargo build and written to dist/wasm/ automatically — no separate build step is needed.
See Islands Architecture for the complete guide.
Architecture
This page provides a technical overview of Taxus's architecture, including the workspace structure, module organization, build pipeline, and data flow.
Workspace Structure
Taxus is organized as a multi-crate Cargo workspace with three crates:
taxus/
├── taxus-client/ # WASM hydration client
├── taxus-common/ # Shared Yew components
└── taxus-generator/ # SSG library and CLI binary
Crate Responsibilities
| Crate | Role | Output |
|---|---|---|
taxus-common | Shared Yew components used by both SSR (generator) and hydration (client) | Library |
taxus-generator | Static site generation: config, content parsing, route discovery, Tera rendering, asset processing | Library (taxus_lib) + Binary (taxus) |
taxus-client | Browser-side WASM that finds island mount points and hydrates them | WASM bundle (embedded in generator binary at compile time) |
Data Flow Between Crates
┌─────────────┐ SSR at build time ┌─────────────┐
│ common │ ──────────────────────────▶│ generator │
│ (components)│ │ (CLI/lib) │
└─────────────┘ └──────┬──────┘
│ │
│ Build output │
│ (HTML + props JSON) │
│ ▼
│ ┌─────────────┐
│ Hydration at runtime │ dist/ │
│ ──────────────────────────────────▶│ (output) │
│ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ client │ ◀─── WASM loads in browser ─│ Browser │
│ (WASM) │ │ │
└─────────────┘ └─────────────┘
Generator Module Map
The generator crate is organized into modules that each own a specific domain:
| Module | Types | Responsibility |
|---|---|---|
config | SiteConfig, SiteMeta, BuildConfig, FeedConfig | Load and validate site.toml configuration |
content | Page, Section, Frontmatter, ContentSource | Parse Markdown files with TOML frontmatter |
routes | RouteDiscovery, RouteRegistry, RouteInfo, RouteKind | Map content files to URL paths |
templates | TeraRenderer, TemplateContext, PageContext, SectionContext, SiteContext | Render HTML with Tera templates |
build | SiteBuilder, BuildReport, ProcessedPage, RenderedPage | Orchestrate the build pipeline (including WASM client writing with islands feature) |
assets | ScssProcessor, StaticCopier, AssetReport | Compile SCSS, copy static files |
feed | FeedGenerator, FeedEntry, FeedConfig | Generate RSS/Atom feeds |
init | InitScaffolder, InitOptions, InitReport | Scaffold new site directories |
serve | DevServer, FileWatcher, WebSocket live reload | Development server with hot reload |
error | GeneratorError + domain sub-errors | Error handling hierarchy |
tracing | init(), init_with_level() | Structured logging setup |
Module Dependencies
┌──────────┐
│ config │
└────┬─────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ content │ │ routes │ │ error │
└────┬────┘ └────┬────┘ └─────────┘
│ │
└─────┬─────┘
▼
┌───────────┐
│ templates │
└─────┬─────┘
│
┌─────┼─────┐
▼ ▼ ▼
┌────────┐ ┌───────┐ ┌──────┐
│ assets │ │ build │ │ feed │
└────────┘ └───┬───┘ └──────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌───────┐
│ init │ │ serve │ │tracing│
└────────┘ └────────┘ └───────┘
Build Pipeline
The SiteBuilder orchestrates a 15-stage build pipeline (13 stages without the islands feature):
┌─────────────────────────────────────────────────────────────────┐
│ SiteBuilder.build() │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [1/13] Discover routes │
│ └──▶ Walk content/ directory │
│ └──▶ Create RouteRegistry (path → content file mapping) │
│ │
│ [2/13] Load templates │
│ └──▶ Read templates/**/*.html │
│ └──▶ Register with Tera (inheritance, island() fn) │
│ │
│ [3/13] Process content │
│ └──▶ Parse frontmatter (TOML) │
│ └──▶ Convert Markdown → HTML │
│ └──▶ Resolve internal links (@/file.md) │
│ └──▶ Produce ProcessedPage for each route │
│ │
│ [4/13] Copy co-located assets │
│ └──▶ Non-.md files in content/ → dist/ │
│ └──▶ Preserve directory structure │
│ │
│ [5/13] Render pages │
│ └──▶ Apply Tera templates to ProcessedPage │
│ └──▶ Handle pagination for sections │
│ └──▶ Produce RenderedPage (final HTML) │
│ │
│ [6/13] Generate robots.txt │
│ └──▶ If no static/robots.txt exists │
│ └──▶ Write default with sitemap reference │
│ │
│ [7/13] Generate sitemap.xml │
│ └──▶ List all routes with lastmod dates │
│ └──▶ Assign priorities (home: 1.0, sections: 0.8, etc) │
│ │
│ [8/13] Generate 404.html │
│ └──▶ Render 404 template if present │
│ │
│ [9/13] Build and render taxonomy pages │
│ └──▶ Extract tags, categories, series from pages │
│ └──▶ Generate /tags/, /tags/slug/, etc │
│ │
│ [10/13] Generate feeds │
│ └──▶ RSS 2.0 (rss_enabled) │
│ └──▶ Atom (atom_enabled) │
│ │
│ [11/13] Process assets │
│ └──▶ Compile SCSS → CSS (styles/**/*.scss) │
│ └──▶ Copy static/ files to dist/static/ │
│ │
│ [12/13] Generate search index (islands feature only) │
│ └──▶ Build TF-IDF index from page content │
│ └──▶ Write dist/search_index.bin │
│ │
│ [13/13] Write WASM client (islands feature only) │
│ └──▶ Write embedded client.js to dist/wasm/ │
│ └──▶ Write embedded client_bg.wasm to dist/wasm/ │
│ │
│ [15/15] Write output │
│ └──▶ Write RenderedPage HTML files │
│ └──▶ Write taxonomy pages │
│ └──▶ Write feed XML files │
│ └──▶ Write alias redirects (HTML meta refresh) │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Types in the Pipeline
RouteRegistry
The first major data structure created. Maps URL paths to content files:
#![allow(unused)] fn main() { RouteRegistry { "/": RouteInfo { kind: Section, content_file: "_index.md", ... }, "/about/": RouteInfo { kind: Page, content_file: "about.md", ... }, "/blog/": RouteInfo { kind: Section, content_file: "blog/_index.md", ... }, "/blog/post/": RouteInfo { kind: Page, content_file: "blog/post.md", ... }, } }
ProcessedPage
The intermediate representation after content parsing:
#![allow(unused)] fn main() { ProcessedPage { route: RouteInfo, page: Page { frontmatter: Frontmatter { title, date, tags, ... }, content: "<p>Rendered HTML from markdown</p>", raw_content: "Original markdown text", }, } }
RenderedPage
The final output after template rendering:
#![allow(unused)] fn main() { RenderedPage { route: RouteInfo, html: "<!DOCTYPE html><html>...</html>", } }
Islands Architecture
When the islands feature is enabled, the generator can pre-render Yew components at build time:
Build-Time (SSR)
- Tera encounters
{{ island(component="Counter", initial=5) | safe }}in a template - The
island()Tera function calls Yew SSR to render the component - Output is wrapped in a mount point with props as JSON:
<div data-island="Counter" data-props='{"initial":5}'>
<!-- Pre-rendered HTML from Yew SSR -->
<div class="counter"><span>5</span><button>+</button></div>
</div>
Browser-Time (Hydration)
- Page loads immediately with pre-rendered HTML (no JavaScript required for initial render)
- WASM bundle (embedded in the generator binary at compile time) loads asynchronously from
/wasm/ - Client finds all
[data-island]elements - For each: deserialize
data-props, callyew::Renderer::hydrate()
See Islands Architecture for the full guide.
Feature Flags
| Feature | Default | Effect |
|---|---|---|
islands | off | Enables Yew SSR, tokio, common crate compilation, and WASM client build/embedding; island() Tera function renders components; WASM client is compiled by build.rs and embedded in the binary |
Without the islands feature:
- Generator is a plain Tera + Markdown SSG
island()function is a no-op (returns empty string)- No Yew or WASM dependencies compiled
- No WASM client embedded or written
# Plain SSG — no Yew
cargo run -- build --dir my-site
# Islands SSG — Yew SSR at build time
cargo run --features islands -- build --dir my-site
Configuration
Taxus uses a site.toml configuration file to define site settings and build options.
Configuration File
Create a site.toml file in your project root:
[site]
name = "My Site"
base_url = "https://example.com"
description = "A description of my site"
author = "Your Name"
[build]
content_dir = "content"
output_dir = "dist"
static_dir = "static"
styles_dir = "styles"
templates_dir = "templates"
[feed]
rss_enabled = true
atom_enabled = false
limit = 20
full_content = false
Configuration Sections
[site] Section
Site metadata and information.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Site name/title |
base_url | string | Yes | Base URL for the site (used for absolute URLs) |
description | string | No | Site description for SEO |
author | string | No | Site author name |
[build] Section
Build configuration options. All fields have defaults.
| Field | Type | Default | Description |
|---|---|---|---|
content_dir | string | "content" | Directory containing Markdown content |
output_dir | string | "dist" | Output directory for generated files |
static_dir | string | "static" | Directory containing static assets |
styles_dir | string | "styles" | Directory containing SCSS stylesheets |
templates_dir | string | "templates" | Directory containing HTML templates |
[feed] Section
RSS/Atom feed configuration for content syndication.
| Field | Type | Default | Description |
|---|---|---|---|
rss_enabled | bool | true | Enable RSS 2.0 feed generation |
atom_enabled | bool | false | Enable Atom feed generation |
limit | number | 0 | Maximum entries in feed (0 = all) |
full_content | bool | false | Include full content vs summary |
title | string | None | Custom feed title (defaults to site name) |
rss_path | string | None | RSS feed output path (default: rss.xml) |
atom_path | string | None | Atom feed output path (default: atom.xml) |
[highlight] Section
Syntax highlighting configuration for code blocks.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable tree-sitter syntax highlighting |
class_prefix | string | "hl-" | CSS class prefix for highlight spans |
See Syntax Highlighting for details on styling and supported languages.
[images] Section
Responsive image processing configuration for hero images.
| Field | Type | Default | Description |
|---|---|---|---|
widths | array | [400, 800, 1200] | Responsive breakpoint widths in pixels |
quality | number | 80 | Output quality (1–100) |
format | string | "webp" | Output format: "webp", "jpeg", or "png" |
output_dir | string | "images" | Subdirectory within dist/ for processed images |
See Images for details on hero images and template usage.
Minimal Configuration
The minimal required configuration:
[site]
name = "My Site"
base_url = "https://example.com"
All [build] and [feed] settings use their default values.
Full Configuration Example
[site]
name = "My Blog"
base_url = "https://example.com"
description = "A blog about Rust and web development"
author = "Jane Doe"
[build]
content_dir = "content"
output_dir = "dist"
static_dir = "static"
styles_dir = "styles"
templates_dir = "templates"
[feed]
rss_enabled = true
atom_enabled = true
limit = 20
full_content = false
title = "My Blog Feed"
rss_path = "rss.xml"
atom_path = "atom.xml"
[highlight]
enabled = true
class_prefix = "hl-"
[images]
widths = [400, 800, 1200]
quality = 80
format = "webp"
output_dir = "images"
Validation
Configuration is validated when loaded:
site.namemust not be emptysite.base_urlmust not be empty
Feed URLs
After generation, feeds are available at:
- RSS:
https://example.com/rss.xml(or customrss_path) - Atom:
https://example.com/atom.xml(or customatom_path)
Content
Content in Taxus is written in Markdown files with TOML frontmatter.
Content Files
Content files are stored in the content/ directory:
content/
├── _index.md # Home page
├── about.md # About page
└── blog/
├── _index.md # Blog section index
├── first-post.md
└── second-post.md
Special Files
| File | Purpose |
|---|---|
_index.md | Section index page (home page at root, section index in subdirectories) |
*.md | Regular pages |
Co-located Assets
Non-Markdown files in the content directory are automatically copied to the output directory, preserving their relative paths. This allows you to keep images and other assets alongside the content that uses them.
content/
├── blog/
│ ├── first-post.md
│ ├── photo.jpg → dist/blog/photo.jpg
│ └── diagrams/
│ └── architecture.png → dist/blog/diagrams/architecture.png
└── about/
├── about.md
└── headshot.png → dist/about/headshot.png
Referencing co-located assets:


Or use absolute paths from the site root:

When to use co-located assets:
- Blog post images and diagrams
- Page-specific downloads (PDFs, etc.)
- Content-specific data files
For global assets (logos, favicons, shared images), use the static/ directory instead.
Hero Images
Pages can have a hero image — a prominent image displayed at the top of the page. Place the image file next to your markdown and reference it in frontmatter:
+++
title = "My Post"
hero_image = "sunset.jpg"
hero_alt = "A dramatic mountain sunset"
date = 2024-03-15
+++
# My Post
Taxus automatically:
- Generates responsive variants at multiple widths (default: 400, 800, 1200)
- Converts to WebP (or JPEG/PNG if configured)
- Produces a
<picture>element with srcset for optimal browser delivery - Falls back to the page title if
hero_altis not provided
See Images for full configuration and template usage.
Frontmatter
Each Markdown file can include TOML frontmatter enclosed in +++:
+++
title = "Page Title"
description = "A brief description"
date = 2024-01-15
template = "custom.html"
draft = false
[extra]
author = "John Doe"
tags = ["rust", "web"]
+++
# Page Content
Your markdown content here.
Frontmatter Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | No* | "" | Page title |
description | string | No | None | Page description for SEO |
date | date | No | None | Publication date (YYYY-MM-DD) |
updated | date | No | None | Last updated date |
template | string | No | "page.html" | Template override |
draft | bool | No | false | Draft status |
summary | string | No | None | Custom summary/excerpt |
slug | string | No | None | Custom URL slug |
aliases | array | No | [] | Old URLs that redirect to this page |
tags | array | No | [] | Tags (e.g., ["rust", "web"]) |
categories | array | No | [] | Categories (e.g., ["tutorial"]) |
series | string | No | None | Series name (e.g., "Learning Rust") |
sort_by | string | No | "date" | Sort order for sections: "date", "title", "weight", "none" |
paginate_by | number | No | 0 | Items per page (0 = no pagination) |
paginate_template | string | No | None | Template for paginated pages |
weight | number | No | 0 | Weight for manual ordering |
hero_image | string | No | None | Relative path to a co-located hero image |
hero_alt | string | No | None | Alt text for hero image (falls back to page title) |
extra | table | No | None | Custom metadata |
*Title is recommended but not required by the library.
Date Format
Dates use the TOML date format:
date = 2024-01-15
updated = 2024-02-20
Extra Metadata
The extra field allows custom metadata:
[extra]
author = "Jane Doe"
custom_field = "any value"
Access extra metadata in templates through the extra variable.
URL Path Generation
Content files are mapped to URL paths:
| Content File | URL Path |
|---|---|
content/_index.md | / |
content/about.md | /about/ |
content/blog/_index.md | /blog/ |
content/blog/my-post.md | /blog/my-post/ |
Custom Slugs
Override the default URL path using the slug frontmatter field:
+++
title = "My First Post"
slug = "hello-world"
+++
This creates /blog/hello-world/ instead of /blog/my-first-post/.
Markdown Support
Taxus supports standard Markdown syntax:
Headings
# Heading 1
## Heading 2
### Heading 3
Lists
- Unordered item
- Another item
1. Ordered item
2. Another item
Links
[Link text](https://example.com)
Internal Links
Reference other pages by their content file path with build-time validation:
See my [about page](@/about.md) for more details.
Check out this [blog post](@/blog/first-post.md).
The @/ prefix signals an internal link. Paths are relative to content/.
| Markdown Link | Resolved HTML |
|---|---|
[about page](@/about.md) | <a href="/about/">about page</a> |
[blog post](@/blog/post.md) | <a href="/blog/post/">blog post</a> |
If an internal link references a non-existent file, the build fails with a clear error.
Images

Code
Inline `code` in text.
```rust
fn main() {
println!("Hello, world!");
}
```
Code blocks with a language identifier are highlighted using tree-sitter. See Syntax Highlighting for configuration and supported languages.
Blockquotes
> This is a blockquote.
Blog Features
Summary and Excerpt
Taxus automatically extracts a summary for each page:
- Automatic extraction: First paragraph of content
- Manual marker: Use
<!-- more -->to mark where summary ends - Frontmatter override: Set a custom summary in frontmatter
+++
title = "My Post"
summary = "A custom summary for SEO"
+++
This is the first paragraph.
<!-- more -->
The rest appears after the summary...
Access in templates: {{ page.summary }}
Reading Time and Word Count
Each page calculates reading time (200 words/minute) and word count:
<span class="reading-time">{{ page.reading_time }} min read</span>
<span class="word-count">{{ page.word_count }} words</span>
Taxonomies
Taxus supports three taxonomy types:
Tags
Multiple keywords associated with a page:
+++
title = "Introduction to Rust"
tags = ["rust", "programming", "tutorial"]
+++
Categories
Broader classifications (also multiple):
+++
title = "My Tutorial"
categories = ["tutorial", "beginner"]
+++
Series
Groups related posts in a sequence (single value):
+++
title = "Part 1: Getting Started"
series = "Learning Rust"
+++
Taxonomy Pages
Taxus generates taxonomy listing and term pages automatically when the corresponding templates exist. The scaffold (taxus init) creates all six templates:
| Template | URL | Purpose |
|---|---|---|
tags.html | /tags/ | Lists all tags |
tags_term.html | /tags/rust/ | Lists pages with the "rust" tag |
categories.html | /categories/ | Lists all categories |
categories_term.html | /categories/tutorial/ | Lists pages in "tutorial" category |
series.html | /series/ | Lists all series |
series_term.html | /series/learning-rust/ | Lists pages in "Learning Rust" series |
If a template is missing, that particular page is skipped silently. See Templates for the full taxonomy template context and examples.
Pagination
Enable pagination in a section's _index.md:
+++
title = "Blog"
sort_by = "date"
paginate_by = 10
+++
# Blog
Welcome to my blog!
Pagination Configuration
| Field | Type | Default | Description |
|---|---|---|---|
sort_by | string | "date" | Sort order: "date", "weight", "title", "none" |
paginate_by | number | 0 | Pages per slice (0 = no pagination) |
paginate_template | string | None | Template for paginated pages |
Pagination URLs
/blog/— First page/blog/page/2/— Second page/blog/page/3/— Third page
Pagination in Templates
{% if section.pagination %}
<nav class="pagination">
{% if section.pagination.prev %}
<a href="{{ section.pagination.prev }}">← Previous</a>
{% endif %}
<span>Page {{ section.pagination.current }} of {{ section.pagination.total }}</span>
{% if section.pagination.next %}
<a href="{{ section.pagination.next }}">Next →</a>
{% endif %}
</nav>
{% endif %}
RSS/Atom Feeds
Configure feeds in site.toml:
[feed]
rss_enabled = true
atom_enabled = true
limit = 20
full_content = false
Feed Entry Fields
| Field | Source |
|---|---|
| Title | Page title |
| Description | Page description or auto-extracted summary |
| URL | Full page URL (base_url + path) |
| Published | Page date field |
| Updated | Page updated field (Atom only) |
Feed URLs
- RSS:
https://example.com/rss.xml - Atom:
https://example.com/atom.xml
Sitemap Generation
Taxus generates sitemap.xml automatically:
- All routes included (pages and sections)
- Draft pages excluded
- Last modification date from page
datefield - Priorities: home
1.0, sections0.8, pages0.7
Sitemap URL
https://example.com/sitemap.xml
Robots.txt Generation
Taxus generates robots.txt automatically if no static/robots.txt exists:
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
To provide a custom robots.txt, create it in static/.
Templates
Templates define the HTML structure for rendered pages using the Tera template engine.
Template Location
Templates are stored in the templates/ directory:
templates/
├── base.html # Base template with common structure
├── page.html # Single page template
├── section.html # Section/list template (e.g., blog)
├── tags.html # Tag listing page (all tags)
├── tags_term.html # Individual tag page (e.g., /tags/rust/)
├── categories.html # Category listing page (all categories)
├── categories_term.html # Individual category page (e.g., /categories/tutorial/)
├── series.html # Series listing page (all series)
├── series_term.html # Individual series page (e.g., /series/learning-rust/)
└── 404.html # Not found page
Template Engine
Taxus uses Tera, a Jinja2-like template engine for Rust:
- Variables:
{{ variable }}syntax - Filters:
{{ content | safe }}for unescaped HTML - Conditionals:
{% if condition %}...{% endif %} - Loops:
{% for item in items %}...{% endfor %} - Template Inheritance:
{% extends "base.html" %}and{% block name %} - Includes:
{% include "partial.html" %}
Base Template
The base template defines the common HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}{{ site.name }}{% endblock %}</title>
{% if page.permalink %}
<link rel="canonical" href="{{ page.permalink }}" />
<meta property="og:url" content="{{ page.permalink }}" />
{% endif %}
<link rel="stylesheet" href="/css/main.css" />
<link rel="icon" href="/static/favicon.png" />
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about/">About</a>
</nav>
</header>
<main>{% block content %}{% endblock %}</main>
<footer>
<p>© {{ now.year }} {{ site.author | default(value="") }}</p>
</footer>
<script src="/static/scripts.js"></script>
</body>
</html>
Page Template
Page templates extend the base template:
{% extends "base.html" %}
{% block title %}{{ page.title }} - {{ site.name }}{% endblock %}
{% block content %}
<article>
{% if page.hero %}
<picture>
<source srcset="{{ page.hero.srcset | safe }}" type="{{ page.hero.mime_type }}">
<img src="{{ page.hero.src | safe }}" alt="{{ page.hero.alt }}"
width="{{ page.hero.width }}" height="{{ page.hero.height }}"
loading="eager" decoding="async">
</picture>
{% endif %}
<h1>{{ page.title }}</h1>
{% if page.description %}
<p class="description">{{ page.description }}</p>
{% endif %}
{% if page.date %}
<time datetime="{{ page.date }}">{{ page.date }}</time>
{% endif %}
{% if page.tags %}
<div class="tags">
{% for tag in page.tags %}
<a href="/tags/{{ tag | slugify }}/">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
<div class="content">{{ page.content | safe }}</div>
</article>
{% endblock %}
Section Template
Section templates render lists of pages:
{% extends "base.html" %}
{% block title %}{{ section.title }} - {{ site.name }}{% endblock %}
{% block content %}
<section>
<h1>{{ section.title }}</h1>
{% if section.description %}
<p class="description">{{ section.description }}</p>
{% endif %}
{% if section.content %}
<div class="section-content">{{ section.content | safe }}</div>
{% endif %}
<ul class="page-list">
{% for page in section.pages %}
<li>
<a href="{{ page.path }}">
<span class="title">{{ page.title }}</span>
{% if page.date %}
<time datetime="{{ page.date }}">{{ page.date }}</time>
{% endif %}
<span class="reading-time">{{ page.reading_time }} min read</span>
</a>
{% if page.summary %}
<p class="summary">{{ page.summary }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% if section.pagination %}
<nav class="pagination">
{% if section.pagination.prev %}
<a href="{{ section.pagination.prev }}">← Previous</a>
{% endif %}
<span>Page {{ section.pagination.current }} of {{ section.pagination.total }}</span>
{% if section.pagination.next %}
<a href="{{ section.pagination.next }}">Next →</a>
{% endif %}
</nav>
{% endif %}
</section>
{% endblock %}
Taxonomy Templates
Taxus generates taxonomy listing and term pages when the corresponding templates exist. The scaffold (taxus init) creates all six templates automatically.
Taxonomy List Templates
List templates render all terms for a taxonomy kind:
tags.html—/tags/categories.html—/categories/series.html—/series/
Each receives extra.taxonomy with:
| Variable | Type | Description |
|---|---|---|
extra.taxonomy.kind | String | Taxonomy kind: "Tags", "Categories", or "Series" |
extra.taxonomy.path | String | URL path (e.g., "/tags/") |
extra.taxonomy.terms | Array | List of term contexts |
Example tags.html:
{% extends "base.html" %}
{% block title %}Tags - {{ site.name }}{% endblock %}
{% block content %}
<section>
<h1>Tags</h1>
{% if extra.taxonomy.terms %}
<ul>
{% for term in extra.taxonomy.terms %}
<li>
<a href="{{ term.path }}">{{ term.name }} ({{ term.page_count }})</a>
</li>
{% endfor %}
</ul>
{% endif %}
</section>
{% endblock %}
Taxonomy Term Templates
Term templates render pages for a specific term:
tags_term.html—/tags/rust/categories_term.html—/categories/tutorial/series_term.html—/series/learning-rust/
Each receives extra.taxonomy with:
| Variable | Type | Description |
|---|---|---|
extra.taxonomy.kind | String | Taxonomy kind: "Tags", "Categories", or "Series" |
extra.taxonomy.name | String | Display name (e.g., "Rust") |
extra.taxonomy.slug | String | URL-safe slug (e.g., "rust") |
extra.taxonomy.path | String | URL path (e.g., "/tags/rust/") |
extra.taxonomy.page_count | Number | Number of pages with this term |
extra.taxonomy.pages | Array | List of page objects with title, path, description, etc. |
Example tags_term.html:
{% extends "base.html" %}
{% block title %}Tag: {{ extra.taxonomy.name }} - {{ site.name }}{% endblock %}
{% block content %}
<section>
<h1>Tagged "{{ extra.taxonomy.name }}"</h1>
<p>{{ extra.taxonomy.page_count }} post(s)</p>
<ul>
{% for page in extra.taxonomy.pages %}
<li><a href="{{ page.path }}">{{ page.title }}</a></li>
{% endfor %}
</ul>
</section>
{% endblock %}
Term Context in List Templates
When iterating extra.taxonomy.terms, each term has:
| Variable | Type | Description |
|---|---|---|
term.name | String | Display name |
term.slug | String | URL-safe slug |
term.path | String | URL path |
term.page_count | Number | Number of pages |
Available Variables
Site Context
| Variable | Type | Description |
|---|---|---|
site.name | String | Site name from configuration |
site.base_url | String | Base URL from configuration |
site.description | String? | Optional site description |
site.author | String? | Optional site author |
Page Context
| Variable | Type | Description |
|---|---|---|
page.title | String | Page title from frontmatter |
page.description | String? | Optional page description |
page.path | String | URL path (e.g., /about/) |
page.permalink | String | Absolute URL (e.g., https://example.com/about/) |
page.content | String | Rendered HTML content |
page.raw_content | String | Raw markdown content |
page.date | String? | Publication date (ISO 8601) |
page.draft | Boolean | Whether page is a draft |
page.summary | String | Summary/excerpt for the page |
page.word_count | Number | Word count |
page.reading_time | Number | Estimated reading time in minutes |
page.tags | Array | Tags for the page |
page.categories | Array | Categories for the page |
page.series | String? | Series name |
page.hero | Object? | Hero image context (see below) |
Hero Image Context
When a page has hero_image in its frontmatter, page.hero contains:
| Variable | Type | Description |
|---|---|---|
page.hero.src | String | Fallback <img> src (middle variant) |
page.hero.srcset | String | Full srcset string for <source> |
page.hero.width | Number | Original image width |
page.hero.height | Number | Original image height |
page.hero.alt | String | Alt text (from hero_alt, or page title) |
page.hero.mime_type | String | MIME type (e.g., "image/webp") |
Example usage:
{% if page.hero %}
<picture>
<source srcset="{{ page.hero.srcset | safe }}" type="{{ page.hero.mime_type }}">
<img src="{{ page.hero.src | safe }}" alt="{{ page.hero.alt }}"
width="{{ page.hero.width }}" height="{{ page.hero.height }}"
loading="eager" decoding="async">
</picture>
{% endif %}
See Images for the complete guide.
Section Context
| Variable | Type | Description |
|---|---|---|
section.title | String | Section title |
section.description | String? | Optional section description |
section.path | String | Section URL path |
section.content | String? | Section HTML content |
section.pages | Array | List of pages in section |
section.pagination | Object? | Pagination information |
Pagination Context
| Variable | Type | Description |
|---|---|---|
section.pagination.current | Number | Current page (1-indexed) |
section.pagination.total | Number | Total pages |
section.pagination.per_page | Number | Items per page |
section.pagination.total_items | Number | Total items across all pages |
section.pagination.prev | String? | URL to previous page |
section.pagination.next | String? | URL to next page |
section.pagination.first | String | URL to first page |
section.pagination.last | String | URL to last page |
Current Date
| Variable | Type | Description |
|---|---|---|
now.year | Number | Current year (e.g., 2024) |
Useful for copyright notices: © {{ now.year }}
Extra Variables
Custom variables from frontmatter extra field:
+++
title = "My Page"
[extra]
author = "John Doe"
custom = "value"
+++
Access in templates:
<p>Author: {{ extra.author }}</p>
<p>Custom: {{ extra.custom }}</p>
Template Inheritance
Templates can extend other templates:
base.html:
<html>
<head>{% block head %}{% endblock %}</head>
<body>{% block body %}{% endblock %}</body>
</html>
page.html:
{% extends "base.html" %}
{% block head %}
<title>{{ page.title }}</title>
{% endblock %}
{% block body %}
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
{% endblock %}
Filters
Commonly used filters:
| Filter | Description |
|---|---|
safe | Output without HTML escaping |
default(value="...") | Provide default value |
upper | Convert to uppercase |
lower | Convert to lowercase |
trim | Remove leading/trailing whitespace |
first | Get first element of array |
last | Get last element of array |
length | Get length of string/array |
join(sep=", ") | Join array with separator |
slugify | Convert to URL-safe slug |
Custom Templates
Pages can specify custom templates in frontmatter:
+++
title = "Special Page"
template = "custom.html"
+++
This page uses custom.html instead of page.html.
Island Components
Use the island() function to embed interactive Yew components:
{% block content %}
{{ page.content | safe }}
{{ island(component="Counter", initial=5) | safe }}
{% endblock %}
Important: Always use | safe after island() to prevent HTML escaping.
See Islands Architecture for the complete guide.
Images
Taxus provides built-in responsive image processing for hero images — automatically generating multiple size variants, converting to modern formats, and producing the <picture> markup needed for optimal delivery.
Hero Images
Hero images are large, prominent images displayed at the top of a page. Taxus handles resizing, format conversion, and responsive markup automatically.
Adding a Hero Image
Place the image file alongside your markdown content (co-located), then reference it in frontmatter:
+++
title = "My Post"
hero_image = "sunset.jpg"
hero_alt = "A dramatic mountain sunset"
date = 2024-03-15
+++
# My Post
Content goes here...
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
hero_image | string | No | None | Relative path to a co-located image file |
hero_alt | string | No | None | Alt text; falls back to page title |
The hero_image path is resolved relative to the content file's directory. If your markdown is at content/blog/my-post.md, then hero_image = "photo.jpg" looks for content/blog/photo.jpg.
What Taxus Does
When a page has a hero_image, the build pipeline:
- Reads the source image and records its dimensions
- Generates responsive variants at each configured width (default: 400, 800, 1200)
- Converts to the configured format (default: WebP)
- Writes variant files to the output directory (default:
dist/images/) - Attaches image metadata to the page's template context as
page.hero
Variant filenames include a content hash for cache-busting:
dist/images/sunset-a3b2c1-400w.webp
dist/images/sunset-a3b2c1-800w.webp
dist/images/sunset-a3b2c1-1200w.webp
If all variants already exist on disk (same source file and hash), processing is skipped — no redundant re-encoding.
Rendering in Templates
The page.hero object is available in page templates when a hero image is set:
{% if page.hero %}
<picture>
<source srcset="{{ page.hero.srcset | safe }}" type="{{ page.hero.mime_type }}">
<img src="{{ page.hero.src | safe }}"
alt="{{ page.hero.alt }}"
width="{{ page.hero.width }}"
height="{{ page.hero.height }}"
loading="eager"
decoding="async">
</picture>
{% endif %}
Important: Use | safe on srcset and src to prevent Tera from HTML-escaping the URLs.
Hero Context Variables
| Variable | Type | Description |
|---|---|---|
page.hero.src | String | Fallback <img> src (middle variant) |
page.hero.srcset | String | Full srcset string for <source> element |
page.hero.width | Number | Original image width (for layout shift prevention) |
page.hero.height | Number | Original image height (for layout shift prevention) |
page.hero.alt | String | Alt text (from hero_alt, or page title as fallback) |
page.hero.mime_type | String | MIME type (e.g., "image/webp") |
Alt Text Fallback
If hero_alt is not set in frontmatter, Taxus falls back to the page title:
+++
title = "Announcing Taxus 1.0"
hero_image = "banner.jpg"
+++
In this case, page.hero.alt will be "Announcing Taxus 1.0".
Image Configuration
Configure image processing in site.toml under the [images] section:
[images]
widths = [400, 800, 1200]
quality = 80
format = "webp"
output_dir = "images"
| Field | Type | Default | Description |
|---|---|---|---|
widths | array | [400, 800, 1200] | Responsive breakpoint widths in pixels |
quality | number | 80 | Output quality (1–100) |
format | string | "webp" | Output format: "webp", "jpeg", or "png" |
output_dir | string | "images" | Subdirectory within dist/ for processed images |
Omitting the Section
If [images] is not present in site.toml, all defaults are used.
How It Works
Build Pipeline
Image processing runs as Stage 4 of the build pipeline, between content processing and co-located asset copying:
- Discover routes → 2. Load templates → 3. Process content → 4. Process images → 5. Copy co-located assets → ...
This means hero image variants are generated before assets are copied and pages are rendered, ensuring the image metadata is available in template context.
Caching
The image processor uses content-hash-based filenames. If all expected variant files already exist on disk with the correct hash, the processor skips re-encoding and rebuilds the metadata from the cache. This makes subsequent builds fast.
Small Source Images
If the source image is smaller than a configured breakpoint width, Taxus does not upscale it. Instead, the original dimensions are used for that variant, preventing quality loss from upscaling.
Dry Run
When running taxus build --dry-run, the image processor calculates metadata and variant paths without reading pixel data or writing files. This allows you to inspect what would be generated without the I/O cost.
Syntax Highlighting
Taxus provides syntax highlighting for code blocks using tree-sitter, a fast and accurate parsing library.
Overview
Code blocks in Markdown are automatically highlighted during the build process. Tree-sitter provides:
- Accurate parsing: Uses real language grammars, not regex patterns
- Fast performance: Incremental parsing for quick builds
- Rich highlighting: Detailed semantic token classification
Usage
Add a language identifier to your fenced code blocks:
```rust
fn main() {
println!("Hello, world!");
}
```
This renders with syntax highlighting:
fn main() { println!("Hello, world!"); }
Supported Languages
Languages are enabled via Cargo features when building Taxus:
| Language | Identifier | Aliases |
|---|---|---|
| Rust | rust | rs |
Additional languages can be added by enabling more tree-sitter grammar features.
Configuration
Configure syntax highlighting in site.toml:
[highlight]
enabled = true
class_prefix = "hl-"
Configuration Options
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable or disable syntax highlighting |
class_prefix | string | "hl-" | CSS class prefix for highlight spans |
Disabling Highlighting
To disable highlighting globally:
[highlight]
enabled = false
Code blocks will still render, but without syntax highlighting spans.
Custom Class Prefix
Use a custom prefix for CSS classes:
[highlight]
class_prefix = "syntax-"
This generates classes like syntax-keyword, syntax-string, etc.
Highlight Classes
Taxus generates semantic CSS classes for each token type:
| Class | Description |
|---|---|
hl-keyword | Keywords (fn, let, struct, impl, etc.) |
hl-string | String literals |
hl-string-special | Special strings (raw strings, format strings) |
hl-comment | Comments |
hl-function | Function names |
hl-function-builtin | Built-in functions |
hl-function-macro | Macro invocations |
hl-type | Type names |
hl-type-builtin | Built-in types (u32, str, etc.) |
hl-constant | Constants |
hl-constant-builtin | Built-in constants |
hl-number | Numeric literals |
hl-constructor | Constructors (Some, Ok, Err, etc.) |
hl-variable | Variables |
hl-variable-builtin | Built-in variables (self, Self) |
hl-variable-parameter | Function parameters |
hl-property | Struct fields/properties |
hl-label | Lifetimes and labels |
hl-attribute | Attributes (#[derive], #[cfg], etc.) |
hl-operator | Operators (=, +, -, etc.) |
hl-punctuation | General punctuation |
hl-punctuation-bracket | Brackets and braces |
hl-punctuation-delimiter | Commas, semicolons |
hl-tag | HTML/XML tags |
Styling
Built-in Themes
Taxus includes two highlight themes:
- Light theme:
_highlight-light.scss— GitHub-inspired colors - Dark theme:
_highlight-dark.scss— Catppuccin-inspired colors
Import in your main stylesheet:
// Light theme (default)
@use "highlight-light";
// Or dark theme
@use "highlight-dark";
Custom Themes
Create custom themes by styling the highlight classes:
// Custom syntax highlighting theme
.hl-keyword { color: #ff79c6; }
.hl-string { color: #f1fa8c; }
.hl-comment { color: #6272a4; font-style: italic; }
.hl-function { color: #50fa7b; }
.hl-type { color: #8be9fd; }
.hl-number { color: #bd93f9; }
Base Styles
Include base styles for code blocks:
pre.highlight {
background-color: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.45;
code {
background: none;
padding: 0;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
}
}
Unsupported Languages
When a language is not supported, the code block renders as plain text with HTML escaping:
```brainfuck
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>
```
The code will display in a code block without syntax highlighting, but HTML special characters are properly escaped.
Adding New Languages
To add support for additional languages:
- Add the tree-sitter grammar to
Cargo.tomlas an optional dependency - Create a feature flag for the language
- Add
LanguageSpecregistration inlanguages.rs - Add highlight queries in
queries/<lang>/highlights.scm
Example for adding JavaScript support:
# Cargo.toml
[dependencies]
tree-sitter-javascript = { version = "0.20", optional = true }
[features]
lang-javascript = ["tree-sitter-javascript"]
#![allow(unused)] fn main() { // languages.rs #[cfg(feature = "lang-javascript")] fn register_javascript(&mut self) { let spec = LanguageSpec { name: "javascript", language: tree_sitter_javascript::LANGUAGE.into(), highlight_query: include_str!("queries/javascript/highlights.scm"), injection_query: None, locals_query: None, }; self.register(spec, &["js", "javascript"]); } }
Islands Architecture
Taxus implements the Islands Architecture: Tera templates render the static "sea" of HTML, while Yew components are pre-rendered server-side into "islands" and then hydrated by the WASM client in the browser.
How It Works
BUILD TIME (generator) BROWSER
───────────────────── ───────
1. Tera renders page shell 4. HTML is immediately visible (no JS needed)
2. island() calls Yew SSR 5. WASM loads asynchronously
3. SSR HTML + props JSON emitted 6. Yew hydrates mount points → interactive
Build-Time: Server-Side Rendering
When you use {{ island(component="Counter", initial=5) | safe }} in a template:
- The
island()Tera function is called during template rendering - Yew SSR renders the component to HTML
- The output is wrapped in a mount point div with serialized props:
<div data-island="Counter" data-props='{"initial":5}'>
<!-- Pre-rendered by Yew SSR: -->
<div class="counter"><span>5</span><button>+</button></div>
</div>
Browser-Time: Hydration
When the page loads:
- The pre-rendered HTML is immediately visible (no JavaScript required)
- The WASM bundle loads asynchronously
- The client finds all
[data-island]elements in the DOM - For each island: deserialize
data-props, callyew::Renderer::hydrate() - The component becomes interactive without re-rendering
Two-Tier Interactivity
taxus supports two layers of interactivity:
| Tier | Technology | Use For |
|---|---|---|
| 1 — General | static/scripts.js (vanilla JS) | DOM manipulation, toggles, analytics, lightweight events |
| 2 — Performance | Yew WASM island | Heavy computation, complex reactive state, data-intensive UI |
Use vanilla JS for simple interactions; reserve Yew islands for components that benefit from the Yew component model.
Using Islands in Templates
The island() Tera Function
Use the island() function in any .html template:
{% extends "base.html" %}
{% block content %}
{{ page.content | safe }}
<h2>Interactive Counter</h2>
{{ island(component="Counter", initial=5) | safe }}
{% endblock %}
Important: Always use | safe after island() to prevent HTML escaping.
Passing Props
Props are passed as keyword arguments to island():
{{ island(component="Counter", initial=5) | safe }}
{{ island(component="MyWidget", label="Hello", count=3) | safe }}
The props are serialized to JSON and stored in data-props.
The class Prop
All island components accept an optional class prop that appends custom CSS classes to the component's outer <div>:
{{ island(component="SearchBox", class="docs-search") | safe }}
This renders as:
<div data-island="SearchBox" data-props='{"placeholder":"Search...","max_results":5,"class":"docs-search"}' class="search-box docs-search">
<!-- component content -->
</div>
This enables template authors to pass CSS styling hooks for targeting descendant elements without modifying component source.
Writing an Island Component
Island components live in common/src/components/. Their props must implement Serialize and Deserialize:
#![allow(unused)] fn main() { // common/src/components/counter.rs use serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Properties, PartialEq, Clone, Serialize, Deserialize)] pub struct CounterProps { #[prop_or_default] pub initial: i32, #[prop_or_default] pub class: String, } #[function_component(Counter)] pub fn counter(props: &CounterProps) -> Html { let count = use_state(|| props.initial); let on_click = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div class="counter"> <span>{ *count }</span> <button onclick={on_click}>{ "+" }</button> </div> } } }
Export the Component
Add the component module to common/src/components/mod.rs:
#![allow(unused)] fn main() { pub mod counter; }
Registering a New Island
Two registries must be updated in sync:
1. Generator SSR Registry
In generator/src/templates/renderer.rs, add a match arm to the island() Tera function:
#![allow(unused)] fn main() { #[cfg(feature = "islands")] tera.register_function("island", |args: &HashMap<String, tera::Value>| { use tera::Value; let component = args.get("component").and_then(Value::as_str).unwrap_or(""); let html = match component { "Counter" => { use crate::build::pipeline::render_island_counter; use common::components::counter::CounterProps; let initial = args.get("initial").and_then(Value::as_i64).unwrap_or(0) as i32; render_island_counter(CounterProps { initial }) } "SearchBox" => { use crate::build::pipeline::render_search_box; use common::components::search_box::SearchBoxProps; let placeholder = args.get("placeholder") .and_then(Value::as_str) .unwrap_or("Search...") .to_string(); let max_results = args.get("max_results") .and_then(Value::as_i64) .unwrap_or(5) as usize; render_search_box(SearchBoxProps { placeholder, max_results }) } "MyWidget" => { // Add your component here use crate::build::pipeline::render_island_generic; use common::components::my_widget::{MyWidget, MyWidgetProps}; let label = args.get("label") .and_then(Value::as_str) .unwrap_or("") .to_string(); let count = args.get("count") .and_then(Value::as_i64) .unwrap_or(0) as i32; render_island_generic::<MyWidget>( MyWidgetProps { label, count }, "MyWidget" ) } other => format!("<!-- unknown island: {other} -->"), }; Ok(Value::String(html)) }); }
2. Client Hydration Registry
In client/src/main.rs, add a match arm to the hydration function:
#![allow(unused)] fn main() { fn hydrate_island(name: &str, el: HtmlElement, props_json: &str) { match name { "Counter" => { let props: CounterProps = serde_json::from_str(props_json) .unwrap_or(CounterProps { initial: 0 }); yew::Renderer::<Counter>::with_root_and_props(el.into(), props).hydrate(); } "SearchBox" => { let props: SearchBoxProps = serde_json::from_str(props_json) .unwrap_or(SearchBoxProps { placeholder: String::new(), max_results: 5, }); yew::Renderer::<SearchBox>::with_root_and_props(el.into(), props).hydrate(); } "MyWidget" => { let props: MyWidgetProps = serde_json::from_str(props_json) .unwrap_or(MyWidgetProps { label: String::new(), count: 0 }); yew::Renderer::<MyWidget>::with_root_and_props(el.into(), props).hydrate(); } _ => { /* ignore unknown islands */ } } } }
Built-in Islands
Taxus includes two built-in island components:
Counter
A simple counter with increment button. This is an example component demonstrating the islands architecture — useful for testing and learning, but not intended for production use.
SearchBox
A production-ready search component with debounced input and async results. See Search for full documentation.
{{ island(component="SearchBox", placeholder="Search...", max_results=10, class="my-search") | safe }}
Initializing a Site with Islands
Use the --islands flag when creating a new site:
cargo run -- init my-site --islands
This includes the WASM hydration script in the generated templates/base.html:
<script type="module">
import init from '/wasm/client.js';
init();
</script>
Building the WASM Client
The WASM client is compiled automatically when building with the islands feature. A Cargo build script (taxus-generator/build.rs) compiles the taxus-client crate to wasm32-unknown-unknown, runs wasm-bindgen to generate JS bindings, and embeds the resulting client.js and client_bg.wasm into the generator binary via include_bytes!. At site build time, these embedded files are written to dist/wasm/.
No separate build step or external tooling (such as Trunk) is required.
Development Workflow
1. Create a Site with Islands
cargo run -- init my-site --islands --name "My Site" --base-url "https://example.com"
2. Build the Static Site (includes WASM client)
cargo run --features islands -- build --dir my-site --verbose
The WASM client is compiled as part of the Cargo build and embedded in the binary. During the taxus build pipeline, the embedded client.js and client_bg.wasm are written to dist/wasm/ automatically — no separate build step needed.
3. Serve and Test
cargo run -- serve --dir my-site --open
The page should:
- Render immediately from the pre-rendered HTML
- Show the counter with the initial value
- After WASM loads (~1s), the button becomes interactive
Feature Flag
The islands Cargo feature controls whether Yew SSR and the WASM client are compiled in:
# Plain SSG — no Yew, fastest compile, smallest binary
cargo run -- build --dir my-site
# Islands SSG — Yew SSR pre-renders components at build time;
# WASM client is compiled and embedded in the binary automatically
cargo run --features islands -- build --dir my-site
Without the feature:
island()function returns empty string (no error)- No Yew or WASM dependencies compiled
- Templates that use
{{ island(...) | safe }}still render without output
With the feature:
- The WASM client (
taxus-client) is compiled towasm32-unknown-unknownbytaxus-generator/build.rs wasm-bindgenJS bindings are generated at Cargo build time- The resulting
client.jsandclient_bg.wasmare embedded in the generator binary viainclude_bytes! - At site build time, these files are written to
dist/wasm/automatically
Search
The islands feature also enables search index generation. See Search for details.
Search
Taxus provides a built-in search component with client-side full-text search. The SearchBox island component uses TF-IDF (Term Frequency-Inverse Document Frequency) ranking with English stemming.
Overview
When the islands feature is enabled, the build pipeline:
- Generates a search index at
dist/search_index.bin - The
SearchBoxcomponent is available for use in templates
The binary index contains:
- Document metadata — Title, path, summary, tags, and categories for each page
- Inverted index — Mapping from word stems to document IDs with TF-IDF scores
The index is serialized with postcard for compact storage and fast deserialization in the browser.
Enabling Search
Search requires the islands feature:
cargo run --features islands -- build --dir my-site
This generates dist/search_index.bin alongside your static files.
Using the SearchBox Component
The SearchBox island component provides a ready-to-use search interface. Add it to any template:
<div class="search-container">
{{ island(component="SearchBox") | safe }}
</div>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | "Search..." | Placeholder text for the input |
max_results | number | 5 | Maximum number of results to display |
class | string | "" | Custom CSS classes to append to the outer container |
Example with custom props:
{{ island(component="SearchBox", placeholder="Find content...", max_results=10, class="docs-search") | safe }}
Styling
The component uses these CSS classes that you can style:
| Class | Element |
|---|---|
.search-box | Container div |
.search-input | Text input field |
.search-results | Results list (<ul>) |
.search-result | Individual result item (<li>) |
.search-result-link | Result title link |
.search-result-summary | Result summary text |
Use the class prop to add custom classes for styling hooks:
{{ island(component="SearchBox", class="docs-search") | safe }}
Then target the custom class in your SCSS:
.docs-search .search-input {
// Custom styles for docs search input
}
Example SCSS:
.search-container {
max-inline-size: 48rem;
margin-inline: auto;
padding-inline: 1.5rem;
}
.search-input {
font-family: var(--font-mono);
font-size: 0.95rem;
padding: 0.6rem 1rem;
border-radius: 0.5rem;
border: 1px solid var(--border);
background-color: var(--bg-surface);
color: var(--text);
}
.search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.search-result {
background-color: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
}
.search-result-link {
font-family: var(--font-mono);
font-weight: 600;
color: var(--accent);
text-decoration: none;
}
.search-result-summary {
font-size: 0.85rem;
color: var(--text-muted);
}
How It Works
Indexing Pipeline
- Tokenization — Content is split into lowercase words, filtering out words shorter than 3 characters
- Stemming — Words are reduced to their root form using the Porter stemmer (e.g., "programming" → "program")
- TF-IDF Scoring — Each term gets a weight based on:
- Term Frequency (TF) — How often the term appears in a document
- Inverse Document Frequency (IDF) — How rare the term is across all documents
Search Query Processing
When a user searches:
- The query is tokenized and stemmed using the same process
- Each stem's postings are retrieved from the index
- TF-IDF scores are summed for matching documents
- Results are returned sorted by relevance score
Component Architecture
The SearchBox component:
- Uses a 200ms debounce on input to avoid excessive queries
- Requires at least 2 characters before searching
- Calls the
window.wasmBindings.search()function exposed by the WASM client - The WASM client lazily loads the search index on first use
- Results are truncated to
max_resultsand displayed in a list
Output Format
The search index is written to dist/search_index.bin in postcard binary format.
Each SearchDocument in the results contains:
| Field | Description |
|---|---|
id | Unique document identifier |
title | Page title from frontmatter |
path | URL path (e.g., /blog/my-post/) |
summary | Page summary for display |
tags | Tags from frontmatter |
categories | Categories from frontmatter |
API Reference
SearchDocument
#![allow(unused)] fn main() { pub struct SearchDocument { pub id: u32, pub title: String, pub path: String, pub summary: String, pub tags: Vec<String>, pub categories: Vec<String>, } }
SearchIndex
#![allow(unused)] fn main() { pub struct SearchIndex { pub documents: Vec<SearchDocument>, pub index: HashMap<String, Vec<(u32, f32)>>, } }
| Method | Description |
|---|---|
new() -> Self | Create an empty index |
add_document(doc, content) | Add a document with its content |
search(query) -> Vec<&SearchDocument> | Search and return ranked results |
finalize() | Apply IDF weighting (call after all documents added) |
to_bytes() -> Vec<u8> | Serialize to binary |
from_bytes(bytes) -> Self | Deserialize from binary |
Helper Functions
#![allow(unused)] fn main() { pub fn tokenize(text: &str) -> Vec<String> }
Splits text into lowercase tokens, filtering words shorter than 3 characters.
#![allow(unused)] fn main() { pub fn stem(tokens: &[String]) -> Vec<String> }
Applies English Porter stemmer to tokens.
Performance
- Index size — Typically 10-30% of total content size
- Deserialization — Near-instant with postcard format
- Search latency — Sub-millisecond for typical queries
- Lazy loading — Index is loaded only when first search is performed
Limitations
- English only — Stemming is currently English-only
- No phrase search — Queries are treated as bag-of-words
- No highlighting — Results don't include matched snippets
CLI Reference
The taxus binary provides five subcommands for building and managing static sites.
Global Usage
taxus <SUBCOMMAND> [OPTIONS]
taxus build
Build the static site from Markdown content and templates.
taxus build [OPTIONS]
Options:
-d, --dir <PATH> Root directory (must contain site.toml) [default: .]
-v, --verbose Print detailed progress for each build stage
-q, --quiet Suppress all output except errors
--include-drafts Include pages marked draft = true
--dry-run Simulate without writing files
--clean Remove output directory before building
-o, --output <PATH> Override the output directory from site.toml
-h, --help Print help
Examples
# Build from current directory
taxus build
# Build with verbose output
taxus build --verbose
# Build from a specific directory
taxus build --dir ./my-site
# Build including drafts
taxus build --include-drafts
# Dry run (validate without writing)
taxus build --dry-run
# Clean and rebuild
taxus build --clean
# Override output directory
taxus build --output /tmp/preview
Build Pipeline Stages
- Discover routes from
content/ - Load Tera templates from
templates/ - Parse Markdown + frontmatter
- Copy co-located assets
- Render pages with templates
- Generate
robots.txt - Generate
sitemap.xml - Generate
404.html - Build and render taxonomy pages
- Generate feeds (RSS/Atom)
- Process assets (SCSS, static files)
- Generate search index (islands feature only)
- Write WASM client (islands feature only)
- Write output files
taxus clean
Remove all generated files from the output directory.
taxus clean [OPTIONS]
Options:
-d, --dir <PATH> Root directory (must contain site.toml) [default: .]
-h, --help Print help
Examples
# Clean current site
taxus clean
# Clean a site in a different directory
taxus clean --dir ./my-site
taxus init
Initialize a new site with a default directory structure.
taxus init [OPTIONS] [PATH]
Arguments:
[PATH] Directory to initialize [default: .]
Options:
-n, --name <NAME> Site name used in templates and site.toml
-u, --base-url <URL> Base URL (must start with http:// or https://)
-f, --force Initialize even if directory is not empty
-i, --islands Include WASM hydration script in templates
-h, --help Print help
Files Created
| File | Description |
|---|---|
site.toml | Site configuration |
content/_index.md | Home page content |
templates/base.html | Base HTML layout |
templates/page.html | Single-page template |
templates/section.html | Section/listing template |
styles/main.scss | Starter stylesheet |
static/scripts.js | Placeholder scripts file |
static/favicon.png | Placeholder favicon |
Examples
# Initialize in current directory
taxus init
# Initialize in a new directory
taxus init my-site
# Initialize with custom options
taxus init my-site --name "My Blog" --base-url "https://myblog.com"
# Initialize with islands support
taxus init my-site --islands
# Force initialization in non-empty directory
taxus init my-site --force
taxus routes
List all routes that would be discovered from the content directory without building.
taxus routes [OPTIONS]
Options:
-d, --dir <PATH> Root directory (must contain site.toml) [default: .]
-h, --help Print help
Example Output
Routes for "My Site"
────────────────────────────────────────────────────
[section] / → _index.md → index.html
[page] /about/ → about.md → about/index.html
[section] /blog/ → blog/_index.md → blog/index.html
[page] /blog/post/ → blog/post.md → blog/post/index.html
────────────────────────────────────────────────────
Total: 4 routes (2 pages, 2 sections)
Examples
# List routes for current site
taxus routes
# List routes for a specific site
taxus routes --dir ./my-site
taxus serve
Start a development server with live reload.
taxus serve [OPTIONS] [DIR]
Arguments:
[DIR] Root directory (must contain site.toml) [default: .]
Options:
-p, --port <PORT> Port to listen on [default: 3000]
-v, --verbose Print detailed progress for each build stage
-q, --quiet Suppress all output except errors
-o, --open Open browser automatically
-h, --help Print help
The serve command performs an initial build automatically, then watches for file changes.
Examples
# Start on default port
taxus serve
# Start with custom port
taxus serve --port 8080
# Start and open browser
taxus serve --open
# Serve from specific directory
taxus serve ./my-site
# Combined options
taxus serve ./my-site --port 8080 --open --verbose
Error Hints
When a command fails, the CLI prints an actionable hint alongside the error:
| Error | Hint |
|---|---|
site.toml not found | Run taxus init or use --dir |
| No content found | Add .md files to content/, start with content/_index.md |
| Template not found | Check that templates/ contains base.html and page.html |
Logging
Control log output with CLI flags or the RUST_LOG environment variable:
# Default: info level (build progress)
taxus build
# Verbose: debug level (detailed stages)
taxus build --verbose
# Quiet: errors only
taxus build --quiet
# Custom via RUST_LOG
RUST_LOG=debug taxus build
RUST_LOG=taxus_lib=trace taxus build
Log levels:
| Level | Description |
|---|---|
error | Build failures only |
warn | Warnings and errors |
info | Build progress (default) |
debug | Detailed stage information |
trace | Verbose internal diagnostics |
Development Server
The serve command provides a local development server with hot reloading.
Basic Usage
# Start server on default port (3000)
taxus serve
# Start with custom port
taxus serve --port 8080
# Start and open browser automatically
taxus serve --open
# Serve from a different directory
taxus serve ./my-site
The serve command performs an initial build automatically before starting the server.
Command Options
| Option | Short | Default | Description |
|---|---|---|---|
--port | -p | 3000 | Port to listen on |
--verbose | -v | false | Print detailed build progress |
--quiet | -q | false | Suppress all output except errors |
--open | -o | false | Open browser automatically |
Features
Hot Reloading
The server watches for file changes and triggers a rebuild:
- Content files (
.mdincontent/) - Templates (
.htmlintemplates/) - Styles (
.scss/.sassinstyles/) - Static files (
static/) - Configuration (
site.toml)
When a change is detected, the server rebuilds and sends a reload signal to connected browsers via WebSocket.
Live Reload Protocol
- Server starts on the specified port
- HTML pages are injected with a live reload script
- Browser connects to
/__ws__WebSocket endpoint - On file change, server broadcasts reload message
- Browser refreshes automatically
Error Overlay
Build errors are displayed in the browser with:
- Error type and message
- File that caused the error
- Suggested fixes (when available)
The overlay dismisses when the error is resolved.
Graceful Shutdown
Press Ctrl+C to shut down cleanly:
- In-flight requests complete
- WebSocket connections close cleanly
- Build operations are cancelled safely
Workflow
After init
taxus init my-site
cd my-site
taxus serve --open
With build
The serve command runs build internally. For production:
# Development
taxus serve
# Production
taxus build
Troubleshooting
Port Already in Use
taxus serve --port 3001
Check what's using the port:
# Linux/macOS
lsof -i :3000
# Windows
netstat -ano | findstr :3000
Files Not Being Watched
Ensure files are in correct directories:
- Content:
content/with.mdextension - Templates:
templates/with.htmlextension - Styles:
styles/with.scssor.sass - Static:
static/
Browser Not Refreshing
- Check WebSocket connection in dev tools (Network → WS)
- Ensure JavaScript is enabled
- Check for console errors
- Verify live reload script is injected (view source)
Styling
Taxus supports SCSS for modern CSS authoring.
Styles Directory
SCSS files are stored in the styles/ directory:
styles/
└── main.scss
SCSS Compilation
The generator compiles SCSS to CSS during the build process:
- Read SCSS files from
styles/ - Compile to CSS using
grass - Write to
dist/css/
Example Stylesheet
styles/main.scss:
// Main stylesheet
// Variables
$primary-color: #0066cc;
$text-color: #333;
$background: #fff;
// Base styles
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6;
color: $text-color;
background: $background;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
// Headings
h1,
h2,
h3 {
margin-top: 1.5em;
color: darken($text-color, 10%);
}
// Links
a {
color: $primary-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// Code blocks
pre {
background: #f4f4f4;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
code {
font-family: "Consolas", "Monaco", monospace;
font-size: 0.9em;
}
SCSS Features
Variables
$primary: #0066cc;
$spacing: 1rem;
.button {
background: $primary;
padding: $spacing;
}
Nesting
nav {
ul {
list-style: none;
}
li {
display: inline-block;
}
a {
color: $primary;
}
}
Partials
Split styles into multiple files:
styles/
├── main.scss # Main file
├── _variables.scss # Variables
├── _base.scss # Base styles
├── _nav.scss # Navigation
└── _footer.scss # Footer
Import in main file:
@use "variables";
@use "base";
@use "nav";
@use "footer";
Mixins
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.container {
@include flex-center;
}
Output
Compiled CSS is written to:
dist/
└── css/
└── main.css
Syntax Highlighting Styles
Taxus includes built-in themes for syntax highlighting. Import them in your main stylesheet:
// Light theme (GitHub-inspired)
@use "highlight-light";
// Or dark theme (Catppuccin-inspired)
@use "highlight-dark";
These themes style the hl-* classes generated by tree-sitter syntax highlighting. See Syntax Highlighting for the full list of highlight classes and customization options.
Linking Styles
Include the stylesheet in your template:
<link rel="stylesheet" href="/css/main.css" />
Development
For development, you can use the sass CLI for live compilation:
# Install sass
npm install -g sass
# Watch for changes
sass --watch styles/main.scss dist/css/main.css
Future Enhancements
Planned improvements for styling:
- PostCSS: Autoprefixer and other transformations
- CSS minification: Production-ready output
- Source maps: Debug support
- CSS modules: Scoped styles for components
Development
This guide covers development workflows for contributing to Taxus.
Prerequisites
- Rust — Install Rust
- mdbook — Documentation:
cargo install mdbook
Setup
git clone https://github.com/crustyrustacean/taxus.git
cd taxus
cargo build
Running Tests
# Run all tests (238+ tests)
cargo test
# Run tests for a specific crate
cargo test -p taxus
# Run unit tests only
cargo test --lib
# Run integration tests only
cargo test --test config_loading
Building
# Build static site (plain SSG)
cargo run -- build --dir my-site
# Build with islands support (WASM client is compiled and embedded automatically)
cargo run --features islands -- build --dir my-site
# Build release binary
cargo build --release
Development Server
# Start server with auto-reload
cargo run -- serve --dir my-site --open
Documentation
cd docs
mdbook serve
Open http://localhost:3000 to view.
Code Commands
| Command | Description |
|---|---|
cargo build | Build all crates |
cargo test | Run all tests |
cargo run -- build | Build the static site |
cargo run -- serve | Start dev server |
cargo doc | Generate API docs |
cargo clippy | Run linter |
cargo fmt | Format code |
Logging
Control log output with CLI flags or RUST_LOG:
# Default: info level
cargo run -- build
# Verbose: debug level
cargo run -- build --verbose
# Quiet: errors only
cargo run -- build --quiet
# Custom via RUST_LOG
RUST_LOG=debug cargo run -- build
RUST_LOG=taxus_lib=trace cargo run -- build
Add logging to code:
#![allow(unused)] fn main() { use tracing::{info, debug, warn, error}; fn build_site() { info!("Building site"); debug!("Processing content"); // Structured fields info!(pages = 5, sections = 2, "Build complete"); } }
Workspace Structure
taxus/
├── taxus-client/ # WASM hydration client
├── taxus-common/ # Shared Yew components
├── taxus-generator/ # SSG library and CLI
└── docs/ # mdBook documentation
Contributing
- Fork the repository
- Create a feature branch
- Make changes
- Run tests:
cargo test - Run linter:
cargo clippy - Format:
cargo fmt - Submit a pull request
API Reference
This page documents the public API of the taxus_lib generator library and taxus-common shared library.
taxus-common Crate
search Module
The search module provides full-text search with TF-IDF ranking.
SearchDocument
#![allow(unused)] fn main() { pub struct SearchDocument { pub id: u32, pub title: String, pub path: String, pub summary: String, pub tags: Vec<String>, pub categories: Vec<String>, } }
Metadata record for each indexed page.
| Method | Description |
|---|---|
new(id, title, path, summary, tags, categories) -> Self | Create a new document |
SearchIndex
#![allow(unused)] fn main() { pub struct SearchIndex { pub documents: Vec<SearchDocument>, pub index: HashMap<String, Vec<(u32, f32)>>, } }
The main search index structure. The index maps word stems to (document_id, tfidf_score) pairs.
| Method | Description |
|---|---|
new() -> Self | Create empty index |
add_document(&mut self, doc: SearchDocument, content: &str) | Add a document with its content for indexing |
search(&self, query: &str) -> Vec<&SearchDocument> | Search and return ranked results |
finalize(&mut self) | Apply IDF weighting (call after all documents added) |
to_bytes(&self) -> Vec<u8> | Serialize to binary (postcard format) |
from_bytes(bytes: &[u8]) -> Self | Deserialize from binary |
Helper Functions
#![allow(unused)] fn main() { pub fn tokenize(text: &str) -> Vec<String> }
Split text into lowercase tokens. Filters words shorter than 3 characters.
#![allow(unused)] fn main() { pub fn stem(tokens: &[String]) -> Vec<String> }
Apply English Porter stemmer to tokens.
taxus-generator Crate
Re-exports
The library re-exports commonly used types from lib.rs:
#![allow(unused)] fn main() { // Configuration pub use config::{BuildConfig, SiteConfig, SiteMeta}; // Content pub use content::{ContentSource, FilesystemContentSource, Frontmatter, Page, Section}; // Templates pub use templates::{ PageContext, SectionContext, SiteContext, TemplateContext, TemplateRenderer, TeraRenderer, }; // Assets pub use assets::{AssetProcessor, AssetReport, ScssProcessor, StaticCopier}; // Build pub use build::{BuildReport, ProcessedPage, RenderedPage, SiteBuilder}; // Feed pub use feed::{FeedConfig, FeedEntry, FeedGenerator}; // Init pub use init::{InitOptions, InitReport, InitScaffolder}; // Routes pub use routes::{RouteDiscovery, RouteInfo, RouteKind, RouteRegistry}; // Errors pub use error::{ AssetError, ContentError, FeedError, GeneratorError, InitError, Result, RouteError, TemplateError, }; }
config Module
SiteConfig
#![allow(unused)] fn main() { pub struct SiteConfig { pub site: SiteMeta, pub build: BuildConfig, pub feed: FeedConfig, pub base_dir: PathBuf, } }
| Method | Description |
|---|---|
from_file(path: P) -> Result<Self> | Load from file |
from_dir(dir: P) -> Result<Self> | Load from directory (looks for site.toml) |
new(name, base_url) -> Self | Create programmatically |
validate(&self) -> Result<()> | Validate required fields |
SiteMeta
#![allow(unused)] fn main() { pub struct SiteMeta { pub name: String, pub base_url: String, pub description: Option<String>, pub author: Option<String>, } }
BuildConfig
#![allow(unused)] fn main() { pub struct BuildConfig { pub content_dir: PathBuf, // default: "content" pub output_dir: PathBuf, // default: "dist" pub static_dir: PathBuf, // default: "static" pub styles_dir: PathBuf, // default: "styles" pub templates_dir: PathBuf, // default: "templates" } }
FeedConfig
#![allow(unused)] fn main() { pub struct FeedConfig { pub rss_enabled: bool, // default: true pub atom_enabled: bool, // default: false pub limit: usize, // default: 0 (all) pub full_content: bool, // default: false pub title: Option<String>, pub rss_path: Option<String>, pub atom_path: Option<String>, } }
content Module
Frontmatter
#![allow(unused)] fn main() { pub struct Frontmatter { pub title: String, pub description: Option<String>, pub date: Option<NaiveDate>, pub updated: Option<NaiveDate>, pub template: Option<String>, pub draft: bool, pub summary: Option<String>, pub slug: Option<String>, pub aliases: Vec<String>, pub tags: Vec<String>, pub categories: Vec<String>, pub series: Option<String>, pub extra: Option<toml::Value>, pub sort_by: SortBy, // default: Date pub paginate_by: usize, pub paginate_template: Option<String>, pub weight: i32, } }
| Method | Description |
|---|---|
from_str(s: &str) -> Result<Self, toml::de::Error> | Parse from TOML |
template(&self) -> &str | Get template (default: "page.html") |
Page
#![allow(unused)] fn main() { pub struct Page { pub frontmatter: Frontmatter, pub path: String, pub source: PathBuf, pub raw_content: String, pub content: Option<String>, } }
| Method | Description |
|---|---|
from_file(path: P) -> Result<Self> | Load from Markdown file |
from_str(content: &str, source: &str) -> Result<Self> | Parse from string |
is_draft(&self) -> bool | Check if draft |
url_path(&self) -> String | Get URL path |
aliases(&self) -> &Vec<String> | Get redirect aliases |
Section
#![allow(unused)] fn main() { pub struct Section { pub frontmatter: Frontmatter, pub path: String, pub source: PathBuf, pub content: Option<String>, pub pages: Vec<Page>, } }
| Method | Description |
|---|---|
from_dir(dir: P) -> Result<Self> | Load from directory |
add_page(&mut self, page: Page) | Add a page |
sort_by_date(&mut self) | Sort by date (newest first) |
ContentSource Trait
#![allow(unused)] fn main() { pub trait ContentSource: Send + Sync { fn load(&self, path: &Path) -> Result<String>; fn exists(&self, path: &Path) -> bool; fn list(&self) -> Result<Vec<PathBuf>>; } }
FilesystemContentSource
#![allow(unused)] fn main() { pub struct FilesystemContentSource { /* ... */ } }
| Method | Description |
|---|---|
new(root: P) -> Self | Create with root directory |
routes Module
RouteKind
#![allow(unused)] fn main() { pub enum RouteKind { Page, Section, } }
RouteInfo
#![allow(unused)] fn main() { pub struct RouteInfo { pub path: String, pub content_file: PathBuf, pub output_file: PathBuf, pub kind: RouteKind, } }
RouteRegistry
#![allow(unused)] fn main() { pub struct RouteRegistry { /* ... */ } }
| Method | Description |
|---|---|
new() -> Self | Create empty registry |
register(&mut self, route: RouteInfo) | Register a route |
get(&self, path: &str) -> Option<&RouteInfo> | Get by path |
contains(&self, path: &str) -> bool | Check existence |
len(&self) -> usize | Count routes |
iter(&self) -> impl Iterator<Item = &RouteInfo> | Iterate all |
pages(&self) -> impl Iterator<Item = &RouteInfo> | Iterate pages |
sections(&self) -> impl Iterator<Item = &RouteInfo> | Iterate sections |
RouteDiscovery
#![allow(unused)] fn main() { pub struct RouteDiscovery { /* ... */ } }
| Method | Description |
|---|---|
new(content_dir: P) -> Self | Create with content directory |
discover(&self) -> Result<RouteRegistry> | Discover all routes |
templates Module
TemplateRenderer Trait
#![allow(unused)] fn main() { pub trait TemplateRenderer: Send + Sync { fn render(&self, template: &str, context: &TemplateContext) -> Result<String>; fn register_template(&mut self, name: &str, content: &str) -> Result<()>; fn has_template(&self, name: &str) -> bool; fn load_templates(&mut self, dir: &Path) -> Result<()>; } }
TeraRenderer
#![allow(unused)] fn main() { pub struct TeraRenderer { /* ... */ } }
| Method | Description |
|---|---|
new() -> Result<Self> | Create empty renderer |
from_dir(dir: P) -> Result<Self> | Create and load from directory |
TemplateContext
#![allow(unused)] fn main() { pub struct TemplateContext { pub page: Option<PageContext>, pub section: Option<SectionContext>, pub site: SiteContext, pub now: NowContext, pub extra: HashMap<String, serde_json::Value>, } }
| Method | Description |
|---|---|
new(site: SiteContext) -> Self | Create with site |
with_page(self, page: PageContext) -> Self | Add page |
with_section(self, section: SectionContext) -> Self | Add section |
with_extra(self, extra: HashMap) -> Self | Add extra |
PageContext
#![allow(unused)] fn main() { pub struct PageContext { pub title: String, pub description: Option<String>, pub path: String, pub permalink: String, pub content: String, pub raw_content: String, pub date: Option<String>, pub draft: bool, pub summary: String, pub word_count: usize, pub reading_time: usize, pub tags: Vec<String>, pub categories: Vec<String>, pub series: Option<String>, } }
SectionContext
#![allow(unused)] fn main() { pub struct SectionContext { pub title: String, pub description: Option<String>, pub path: String, pub content: Option<String>, pub pages: Vec<PageContext>, pub pagination: Option<PaginationContext>, } }
PaginationContext
#![allow(unused)] fn main() { pub struct PaginationContext { pub current: usize, pub total: usize, pub per_page: usize, pub total_items: usize, pub prev: Option<String>, pub next: Option<String>, pub first: String, pub last: String, } }
SiteContext
#![allow(unused)] fn main() { pub struct SiteContext { pub name: String, pub base_url: String, pub description: Option<String>, pub author: Option<String>, } }
NowContext
#![allow(unused)] fn main() { pub struct NowContext { pub year: i32, } }
build Module
SiteBuilder
#![allow(unused)] fn main() { pub struct SiteBuilder { config: SiteConfig, dry_run: bool, verbose: bool, include_drafts: bool, } }
| Method | Description |
|---|---|
from_dir(dir: &Path) -> Result<Self> | Create from directory |
new(config: SiteConfig) -> Self | Create from config |
dry_run(self, bool) -> Self | Set dry-run mode |
verbose(self, bool) -> Self | Set verbose mode |
include_drafts(self, bool) -> Self | Include drafts |
build(self) -> Result<BuildReport> | Run build pipeline |
clean(self) -> Result<()> | Clean output directory |
build::pipeline::search Module (islands feature only)
GeneratedSearch
#![allow(unused)] fn main() { pub struct GeneratedSearch { pub search_index: Vec<u8>, } }
Container for the serialized search index.
| Function | Description |
|---|---|
generate_search(pages: &[ProcessedPage]) -> Result<GeneratedSearch> | Create search index from processed pages |
write_search_index(generated: &GeneratedSearch, output_dir: &Path, dry_run: bool) -> Result<()> | Write search_index.bin to output |
build::pipeline::wasm Module (islands feature only)
The WASM client is compiled at Cargo build time by taxus-generator/build.rs and embedded into the binary. At site build time, the embedded files are written to the output directory.
WasmBuildOutput
#![allow(unused)] fn main() { pub struct WasmBuildOutput { pub js_path: PathBuf, pub wasm_path: PathBuf, pub wasm_size: u64, } }
Result of writing the embedded WASM client files.
| Function | Description |
|---|---|
build_wasm_client(output_dir: &Path) -> Result<WasmBuildOutput> | Write embedded client.js and client_bg.wasm to dist/wasm/ |
BuildReport
#![allow(unused)] fn main() { pub struct BuildReport { pub output_dir: PathBuf, pub pages_rendered: usize, pub sections_rendered: usize, pub drafts_skipped: usize, pub sitemap_urls: usize, pub assets: AssetReport, pub duration: Duration, } }
| Method | Description |
|---|---|
print_summary(&self) | Print summary |
has_warnings(&self) -> bool | Check for warnings |
ProcessedPage
#![allow(unused)] fn main() { pub struct ProcessedPage { pub route: RouteInfo, pub page: Page, } }
RenderedPage
#![allow(unused)] fn main() { pub struct RenderedPage { pub route: RouteInfo, pub html: String, } }
assets Module
AssetProcessor Trait
#![allow(unused)] fn main() { pub trait AssetProcessor: Send + Sync { fn process(&self, src: &Path, dest: &Path) -> Result<AssetReport>; fn handles(&self, path: &Path) -> bool; fn name(&self) -> &'static str; } }
ScssProcessor
#![allow(unused)] fn main() { pub struct ScssProcessor { include_paths: Vec<PathBuf>, minify: bool, } }
| Method | Description |
|---|---|
new() -> Self | Create with defaults |
with_include_paths(paths: Vec<PathBuf>) -> Self | Set include paths |
with_minify(bool) -> Self | Set minify |
StaticCopier
#![allow(unused)] fn main() { pub struct StaticCopier { exclude_patterns: Vec<String>, } }
| Method | Description |
|---|---|
new() -> Self | Create with defaults |
with_exclusions(patterns: Vec<String>) -> Self | Set exclusions |
AssetReport
#![allow(unused)] fn main() { pub struct AssetReport { pub files_processed: usize, pub files_skipped: usize, pub errors: Vec<String>, } }
| Method | Description |
|---|---|
merge(&mut self, other: AssetReport) | Merge reports |
init Module
InitOptions
#![allow(unused)] fn main() { pub struct InitOptions { pub name: String, pub base_url: String, pub force: bool, pub islands: bool, } }
| Method | Description |
|---|---|
new(name, base_url) -> Self | Create options |
with_force(bool) -> Self | Set force |
with_islands(bool) -> Self | Set islands |
InitScaffolder
#![allow(unused)] fn main() { pub struct InitScaffolder { /* ... */ } }
| Method | Description |
|---|---|
new(options: InitOptions) -> Self | Create scaffolder |
scaffold(&self, path: &Path) -> Result<InitReport> | Scaffold site |
InitReport
#![allow(unused)] fn main() { pub struct InitReport { pub path: PathBuf, pub directories_created: usize, pub files_created: usize, pub created_dirs: Vec<PathBuf>, pub created_files: Vec<PathBuf>, } }
serve Module
DevServer
#![allow(unused)] fn main() { pub struct DevServer { /* ... */ } }
| Method | Description |
|---|---|
new(config: DevServerConfig) -> Self | Create server |
run(&self) -> Result<()> | Start server (async) |
DevServerConfig
#![allow(unused)] fn main() { pub struct DevServerConfig { pub site_dir: PathBuf, pub port: u16, pub output_dir: PathBuf, } }
| Method | Description |
|---|---|
default() -> Self | Create with defaults |
with_port(self, port: u16) -> Self | Set port |
with_output_dir(self, dir: PathBuf) -> Self | Set output dir |
with_site_dir(self, dir: PathBuf) -> Self | Set site dir |
error Module
GeneratorError
#![allow(unused)] fn main() { pub enum GeneratorError { Config(Box<ConfigError>), Content(Box<ContentError>), Template(Box<TemplateError>), Asset(Box<AssetError>), Route(Box<RouteError>), Init(Box<InitError>), Serve(Box<ServeError>), Feed(Box<FeedError>), Io { path: PathBuf, source: std::io::Error }, NoContent, BrokenInternalLink { file: String, target: String }, PageRenderFailed { path: String, source: TemplateError }, } }
| Type | Description |
|---|---|
ConfigError | Configuration errors (not found, parse, missing field) |
ContentError | Content errors (not found, frontmatter, IO) |
TemplateError | Template errors (not found, render, syntax) |
AssetError | Asset errors (SCSS, copy) |
RouteError | Route errors (not found, duplicate, invalid) |
FeedError | Feed generation errors |
ImageError | Image processing errors |
InitError | Initialization errors (cancelled) |
ServeError | Server errors (port in use, WebSocket) |
WasmError | WASM build errors (tool missing, build failed) |
Result
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, GeneratorError>; }
tracing Module
| Function | Description |
|---|---|
init() | Initialize with RUST_LOG env var |
init_with_level(level: &str) | Initialize with specific level |