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.xml generation 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, and serve subcommands

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

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:

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

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

CrateRoleOutput
taxus-commonShared Yew components used by both SSR (generator) and hydration (client)Library
taxus-generatorStatic site generation: config, content parsing, route discovery, Tera rendering, asset processingLibrary (taxus_lib) + Binary (taxus)
taxus-clientBrowser-side WASM that finds island mount points and hydrates themWASM 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:

ModuleTypesResponsibility
configSiteConfig, SiteMeta, BuildConfig, FeedConfigLoad and validate site.toml configuration
contentPage, Section, Frontmatter, ContentSourceParse Markdown files with TOML frontmatter
routesRouteDiscovery, RouteRegistry, RouteInfo, RouteKindMap content files to URL paths
templatesTeraRenderer, TemplateContext, PageContext, SectionContext, SiteContextRender HTML with Tera templates
buildSiteBuilder, BuildReport, ProcessedPage, RenderedPageOrchestrate the build pipeline (including WASM client writing with islands feature)
assetsScssProcessor, StaticCopier, AssetReportCompile SCSS, copy static files
feedFeedGenerator, FeedEntry, FeedConfigGenerate RSS/Atom feeds
initInitScaffolder, InitOptions, InitReportScaffold new site directories
serveDevServer, FileWatcher, WebSocket live reloadDevelopment server with hot reload
errorGeneratorError + domain sub-errorsError handling hierarchy
tracinginit(), 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)

  1. Tera encounters {{ island(component="Counter", initial=5) | safe }} in a template
  2. The island() Tera function calls Yew SSR to render the component
  3. 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)

  1. Page loads immediately with pre-rendered HTML (no JavaScript required for initial render)
  2. WASM bundle (embedded in the generator binary at compile time) loads asynchronously from /wasm/
  3. Client finds all [data-island] elements
  4. For each: deserialize data-props, call yew::Renderer::hydrate()

See Islands Architecture for the full guide.

Feature Flags

FeatureDefaultEffect
islandsoffEnables 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.

FieldTypeRequiredDescription
namestringYesSite name/title
base_urlstringYesBase URL for the site (used for absolute URLs)
descriptionstringNoSite description for SEO
authorstringNoSite author name

[build] Section

Build configuration options. All fields have defaults.

FieldTypeDefaultDescription
content_dirstring"content"Directory containing Markdown content
output_dirstring"dist"Output directory for generated files
static_dirstring"static"Directory containing static assets
styles_dirstring"styles"Directory containing SCSS stylesheets
templates_dirstring"templates"Directory containing HTML templates

[feed] Section

RSS/Atom feed configuration for content syndication.

FieldTypeDefaultDescription
rss_enabledbooltrueEnable RSS 2.0 feed generation
atom_enabledboolfalseEnable Atom feed generation
limitnumber0Maximum entries in feed (0 = all)
full_contentboolfalseInclude full content vs summary
titlestringNoneCustom feed title (defaults to site name)
rss_pathstringNoneRSS feed output path (default: rss.xml)
atom_pathstringNoneAtom feed output path (default: atom.xml)

[highlight] Section

Syntax highlighting configuration for code blocks.

FieldTypeDefaultDescription
enabledbooltrueEnable tree-sitter syntax highlighting
class_prefixstring"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.

FieldTypeDefaultDescription
widthsarray[400, 800, 1200]Responsive breakpoint widths in pixels
qualitynumber80Output quality (1–100)
formatstring"webp"Output format: "webp", "jpeg", or "png"
output_dirstring"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.name must not be empty
  • site.base_url must not be empty

Feed URLs

After generation, feeds are available at:

  • RSS: https://example.com/rss.xml (or custom rss_path)
  • Atom: https://example.com/atom.xml (or custom atom_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

FilePurpose
_index.mdSection index page (home page at root, section index in subdirectories)
*.mdRegular 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:

![Photo](photo.jpg)
![Diagram](diagrams/architecture.png)

Or use absolute paths from the site root:

![Photo](/blog/photo.jpg)

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_alt is 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

FieldTypeRequiredDefaultDescription
titlestringNo*""Page title
descriptionstringNoNonePage description for SEO
datedateNoNonePublication date (YYYY-MM-DD)
updateddateNoNoneLast updated date
templatestringNo"page.html"Template override
draftboolNofalseDraft status
summarystringNoNoneCustom summary/excerpt
slugstringNoNoneCustom URL slug
aliasesarrayNo[]Old URLs that redirect to this page
tagsarrayNo[]Tags (e.g., ["rust", "web"])
categoriesarrayNo[]Categories (e.g., ["tutorial"])
seriesstringNoNoneSeries name (e.g., "Learning Rust")
sort_bystringNo"date"Sort order for sections: "date", "title", "weight", "none"
paginate_bynumberNo0Items per page (0 = no pagination)
paginate_templatestringNoNoneTemplate for paginated pages
weightnumberNo0Weight for manual ordering
hero_imagestringNoNoneRelative path to a co-located hero image
hero_altstringNoNoneAlt text for hero image (falls back to page title)
extratableNoNoneCustom 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 FileURL 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
[Link text](https://example.com)

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 LinkResolved 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

![Alt text](/images/photo.png)

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:

  1. Automatic extraction: First paragraph of content
  2. Manual marker: Use <!-- more --> to mark where summary ends
  3. 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:

TemplateURLPurpose
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

FieldTypeDefaultDescription
sort_bystring"date"Sort order: "date", "weight", "title", "none"
paginate_bynumber0Pages per slice (0 = no pagination)
paginate_templatestringNoneTemplate 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

FieldSource
TitlePage title
DescriptionPage description or auto-extracted summary
URLFull page URL (base_url + path)
PublishedPage date field
UpdatedPage 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 date field
  • Priorities: home 1.0, sections 0.8, pages 0.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>&copy; {{ 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:

VariableTypeDescription
extra.taxonomy.kindStringTaxonomy kind: "Tags", "Categories", or "Series"
extra.taxonomy.pathStringURL path (e.g., "/tags/")
extra.taxonomy.termsArrayList 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:

VariableTypeDescription
extra.taxonomy.kindStringTaxonomy kind: "Tags", "Categories", or "Series"
extra.taxonomy.nameStringDisplay name (e.g., "Rust")
extra.taxonomy.slugStringURL-safe slug (e.g., "rust")
extra.taxonomy.pathStringURL path (e.g., "/tags/rust/")
extra.taxonomy.page_countNumberNumber of pages with this term
extra.taxonomy.pagesArrayList 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:

VariableTypeDescription
term.nameStringDisplay name
term.slugStringURL-safe slug
term.pathStringURL path
term.page_countNumberNumber of pages

Available Variables

Site Context

VariableTypeDescription
site.nameStringSite name from configuration
site.base_urlStringBase URL from configuration
site.descriptionString?Optional site description
site.authorString?Optional site author

Page Context

VariableTypeDescription
page.titleStringPage title from frontmatter
page.descriptionString?Optional page description
page.pathStringURL path (e.g., /about/)
page.permalinkStringAbsolute URL (e.g., https://example.com/about/)
page.contentStringRendered HTML content
page.raw_contentStringRaw markdown content
page.dateString?Publication date (ISO 8601)
page.draftBooleanWhether page is a draft
page.summaryStringSummary/excerpt for the page
page.word_countNumberWord count
page.reading_timeNumberEstimated reading time in minutes
page.tagsArrayTags for the page
page.categoriesArrayCategories for the page
page.seriesString?Series name
page.heroObject?Hero image context (see below)

Hero Image Context

When a page has hero_image in its frontmatter, page.hero contains:

VariableTypeDescription
page.hero.srcStringFallback <img> src (middle variant)
page.hero.srcsetStringFull srcset string for <source>
page.hero.widthNumberOriginal image width
page.hero.heightNumberOriginal image height
page.hero.altStringAlt text (from hero_alt, or page title)
page.hero.mime_typeStringMIME 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

VariableTypeDescription
section.titleStringSection title
section.descriptionString?Optional section description
section.pathStringSection URL path
section.contentString?Section HTML content
section.pagesArrayList of pages in section
section.paginationObject?Pagination information

Pagination Context

VariableTypeDescription
section.pagination.currentNumberCurrent page (1-indexed)
section.pagination.totalNumberTotal pages
section.pagination.per_pageNumberItems per page
section.pagination.total_itemsNumberTotal items across all pages
section.pagination.prevString?URL to previous page
section.pagination.nextString?URL to next page
section.pagination.firstStringURL to first page
section.pagination.lastStringURL to last page

Current Date

VariableTypeDescription
now.yearNumberCurrent year (e.g., 2024)

Useful for copyright notices: &copy; {{ 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:

FilterDescription
safeOutput without HTML escaping
default(value="...")Provide default value
upperConvert to uppercase
lowerConvert to lowercase
trimRemove leading/trailing whitespace
firstGet first element of array
lastGet last element of array
lengthGet length of string/array
join(sep=", ")Join array with separator
slugifyConvert 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...
FieldTypeRequiredDefaultDescription
hero_imagestringNoNoneRelative path to a co-located image file
hero_altstringNoNoneAlt 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:

  1. Reads the source image and records its dimensions
  2. Generates responsive variants at each configured width (default: 400, 800, 1200)
  3. Converts to the configured format (default: WebP)
  4. Writes variant files to the output directory (default: dist/images/)
  5. 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

VariableTypeDescription
page.hero.srcStringFallback <img> src (middle variant)
page.hero.srcsetStringFull srcset string for <source> element
page.hero.widthNumberOriginal image width (for layout shift prevention)
page.hero.heightNumberOriginal image height (for layout shift prevention)
page.hero.altStringAlt text (from hero_alt, or page title as fallback)
page.hero.mime_typeStringMIME 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"
FieldTypeDefaultDescription
widthsarray[400, 800, 1200]Responsive breakpoint widths in pixels
qualitynumber80Output quality (1–100)
formatstring"webp"Output format: "webp", "jpeg", or "png"
output_dirstring"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:

  1. 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:

LanguageIdentifierAliases
Rustrustrs

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

FieldTypeDefaultDescription
enabledbooltrueEnable or disable syntax highlighting
class_prefixstring"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:

ClassDescription
hl-keywordKeywords (fn, let, struct, impl, etc.)
hl-stringString literals
hl-string-specialSpecial strings (raw strings, format strings)
hl-commentComments
hl-functionFunction names
hl-function-builtinBuilt-in functions
hl-function-macroMacro invocations
hl-typeType names
hl-type-builtinBuilt-in types (u32, str, etc.)
hl-constantConstants
hl-constant-builtinBuilt-in constants
hl-numberNumeric literals
hl-constructorConstructors (Some, Ok, Err, etc.)
hl-variableVariables
hl-variable-builtinBuilt-in variables (self, Self)
hl-variable-parameterFunction parameters
hl-propertyStruct fields/properties
hl-labelLifetimes and labels
hl-attributeAttributes (#[derive], #[cfg], etc.)
hl-operatorOperators (=, +, -, etc.)
hl-punctuationGeneral punctuation
hl-punctuation-bracketBrackets and braces
hl-punctuation-delimiterCommas, semicolons
hl-tagHTML/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:

  1. Add the tree-sitter grammar to Cargo.toml as an optional dependency
  2. Create a feature flag for the language
  3. Add LanguageSpec registration in languages.rs
  4. 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:

  1. The island() Tera function is called during template rendering
  2. Yew SSR renders the component to HTML
  3. 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:

  1. The pre-rendered HTML is immediately visible (no JavaScript required)
  2. The WASM bundle loads asynchronously
  3. The client finds all [data-island] elements in the DOM
  4. For each island: deserialize data-props, call yew::Renderer::hydrate()
  5. The component becomes interactive without re-rendering

Two-Tier Interactivity

taxus supports two layers of interactivity:

TierTechnologyUse For
1 — Generalstatic/scripts.js (vanilla JS)DOM manipulation, toggles, analytics, lightweight events
2 — PerformanceYew WASM islandHeavy 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.

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 to wasm32-unknown-unknown by taxus-generator/build.rs
  • wasm-bindgen JS bindings are generated at Cargo build time
  • The resulting client.js and client_bg.wasm are embedded in the generator binary via include_bytes!
  • At site build time, these files are written to dist/wasm/ automatically

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:

  1. Generates a search index at dist/search_index.bin
  2. The SearchBox component 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.

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

PropTypeDefaultDescription
placeholderstring"Search..."Placeholder text for the input
max_resultsnumber5Maximum number of results to display
classstring""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:

ClassElement
.search-boxContainer div
.search-inputText input field
.search-resultsResults list (<ul>)
.search-resultIndividual result item (<li>)
.search-result-linkResult title link
.search-result-summaryResult 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

  1. Tokenization — Content is split into lowercase words, filtering out words shorter than 3 characters
  2. Stemming — Words are reduced to their root form using the Porter stemmer (e.g., "programming" → "program")
  3. 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:

  1. The query is tokenized and stemmed using the same process
  2. Each stem's postings are retrieved from the index
  3. TF-IDF scores are summed for matching documents
  4. Results are returned sorted by relevance score

Component Architecture

The SearchBox component:

  1. Uses a 200ms debounce on input to avoid excessive queries
  2. Requires at least 2 characters before searching
  3. Calls the window.wasmBindings.search() function exposed by the WASM client
  4. The WASM client lazily loads the search index on first use
  5. Results are truncated to max_results and 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:

FieldDescription
idUnique document identifier
titlePage title from frontmatter
pathURL path (e.g., /blog/my-post/)
summaryPage summary for display
tagsTags from frontmatter
categoriesCategories 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)>>,
}
}
MethodDescription
new() -> SelfCreate 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) -> SelfDeserialize 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

  1. Discover routes from content/
  2. Load Tera templates from templates/
  3. Parse Markdown + frontmatter
  4. Copy co-located assets
  5. Render pages with templates
  6. Generate robots.txt
  7. Generate sitemap.xml
  8. Generate 404.html
  9. Build and render taxonomy pages
  10. Generate feeds (RSS/Atom)
  11. Process assets (SCSS, static files)
  12. Generate search index (islands feature only)
  13. Write WASM client (islands feature only)
  14. 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

FileDescription
site.tomlSite configuration
content/_index.mdHome page content
templates/base.htmlBase HTML layout
templates/page.htmlSingle-page template
templates/section.htmlSection/listing template
styles/main.scssStarter stylesheet
static/scripts.jsPlaceholder scripts file
static/favicon.pngPlaceholder 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:

ErrorHint
site.toml not foundRun taxus init or use --dir
No content foundAdd .md files to content/, start with content/_index.md
Template not foundCheck 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:

LevelDescription
errorBuild failures only
warnWarnings and errors
infoBuild progress (default)
debugDetailed stage information
traceVerbose 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

OptionShortDefaultDescription
--port-p3000Port to listen on
--verbose-vfalsePrint detailed build progress
--quiet-qfalseSuppress all output except errors
--open-ofalseOpen browser automatically

Features

Hot Reloading

The server watches for file changes and triggers a rebuild:

  • Content files (.md in content/)
  • Templates (.html in templates/)
  • Styles (.scss/.sass in styles/)
  • 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

  1. Server starts on the specified port
  2. HTML pages are injected with a live reload script
  3. Browser connects to /__ws__ WebSocket endpoint
  4. On file change, server broadcasts reload message
  5. 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 .md extension
  • Templates: templates/ with .html extension
  • Styles: styles/ with .scss or .sass
  • Static: static/

Browser Not Refreshing

  1. Check WebSocket connection in dev tools (Network → WS)
  2. Ensure JavaScript is enabled
  3. Check for console errors
  4. 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:

  1. Read SCSS files from styles/
  2. Compile to CSS using grass
  3. 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

  • RustInstall 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

CommandDescription
cargo buildBuild all crates
cargo testRun all tests
cargo run -- buildBuild the static site
cargo run -- serveStart dev server
cargo docGenerate API docs
cargo clippyRun linter
cargo fmtFormat 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

  1. Fork the repository
  2. Create a feature branch
  3. Make changes
  4. Run tests: cargo test
  5. Run linter: cargo clippy
  6. Format: cargo fmt
  7. 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.

MethodDescription
new(id, title, path, summary, tags, categories) -> SelfCreate 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.

MethodDescription
new() -> SelfCreate 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]) -> SelfDeserialize 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,
}
}
MethodDescription
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) -> SelfCreate 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,
}
}
MethodDescription
from_str(s: &str) -> Result<Self, toml::de::Error>Parse from TOML
template(&self) -> &strGet 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>,
}
}
MethodDescription
from_file(path: P) -> Result<Self>Load from Markdown file
from_str(content: &str, source: &str) -> Result<Self>Parse from string
is_draft(&self) -> boolCheck if draft
url_path(&self) -> StringGet 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>,
}
}
MethodDescription
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 { /* ... */ }
}
MethodDescription
new(root: P) -> SelfCreate 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 { /* ... */ }
}
MethodDescription
new() -> SelfCreate empty registry
register(&mut self, route: RouteInfo)Register a route
get(&self, path: &str) -> Option<&RouteInfo>Get by path
contains(&self, path: &str) -> boolCheck existence
len(&self) -> usizeCount 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 { /* ... */ }
}
MethodDescription
new(content_dir: P) -> SelfCreate 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 { /* ... */ }
}
MethodDescription
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>,
}
}
MethodDescription
new(site: SiteContext) -> SelfCreate with site
with_page(self, page: PageContext) -> SelfAdd page
with_section(self, section: SectionContext) -> SelfAdd section
with_extra(self, extra: HashMap) -> SelfAdd 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,
}
}
MethodDescription
from_dir(dir: &Path) -> Result<Self>Create from directory
new(config: SiteConfig) -> SelfCreate from config
dry_run(self, bool) -> SelfSet dry-run mode
verbose(self, bool) -> SelfSet verbose mode
include_drafts(self, bool) -> SelfInclude 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.

FunctionDescription
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.

FunctionDescription
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,
}
}
MethodDescription
print_summary(&self)Print summary
has_warnings(&self) -> boolCheck 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,
}
}
MethodDescription
new() -> SelfCreate with defaults
with_include_paths(paths: Vec<PathBuf>) -> SelfSet include paths
with_minify(bool) -> SelfSet minify

StaticCopier

#![allow(unused)]
fn main() {
pub struct StaticCopier {
    exclude_patterns: Vec<String>,
}
}
MethodDescription
new() -> SelfCreate with defaults
with_exclusions(patterns: Vec<String>) -> SelfSet exclusions

AssetReport

#![allow(unused)]
fn main() {
pub struct AssetReport {
    pub files_processed: usize,
    pub files_skipped: usize,
    pub errors: Vec<String>,
}
}
MethodDescription
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,
}
}
MethodDescription
new(name, base_url) -> SelfCreate options
with_force(bool) -> SelfSet force
with_islands(bool) -> SelfSet islands

InitScaffolder

#![allow(unused)]
fn main() {
pub struct InitScaffolder { /* ... */ }
}
MethodDescription
new(options: InitOptions) -> SelfCreate 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 { /* ... */ }
}
MethodDescription
new(config: DevServerConfig) -> SelfCreate 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,
}
}
MethodDescription
default() -> SelfCreate with defaults
with_port(self, port: u16) -> SelfSet port
with_output_dir(self, dir: PathBuf) -> SelfSet output dir
with_site_dir(self, dir: PathBuf) -> SelfSet 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 },
}
}
TypeDescription
ConfigErrorConfiguration errors (not found, parse, missing field)
ContentErrorContent errors (not found, frontmatter, IO)
TemplateErrorTemplate errors (not found, render, syntax)
AssetErrorAsset errors (SCSS, copy)
RouteErrorRoute errors (not found, duplicate, invalid)
FeedErrorFeed generation errors
ImageErrorImage processing errors
InitErrorInitialization errors (cancelled)
ServeErrorServer errors (port in use, WebSocket)
WasmErrorWASM build errors (tool missing, build failed)

Result

#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, GeneratorError>;
}

tracing Module

FunctionDescription
init()Initialize with RUST_LOG env var
init_with_level(level: &str)Initialize with specific level