Built-in Plugins Reference ¶
This reference documents all built-in plugins that ship with markata-go. Plugins are organized by their lifecycle stage.
Related guides:
- Plugin Development - Create your own plugins
- [[configuration-guide|Configuration]] - Configure plugins
- [[feeds-guide|Feeds]] - Feed plugins in detail
Plugin Lifecycle Overview ¶ #
Configure -> Glob -> Load -> Transform -> Render -> Collect -> Write -> Cleanup
| Stage | Purpose | Example Plugins |
|---|---|---|
| Configure | Initialize plugin settings | templates |
| Glob | Discover content files | glob |
| Load | Parse files into posts | load, frontmatter |
| Transform | Pre-render modifications | description, reading_time, stats, breadcrumbs, jinja_md, wikilinks, toc |
| Render | Convert content to HTML | render_markdown, templates, admonitions, heading_anchors, link_collector, mermaid, glossary, csv_fence, youtube |
| Collect | Build collections/feeds | series, feeds, auto_feeds, prevnext, overwrite_check, static_file_conflicts |
| Write | Output files to disk | publish_html, random_post, publish_feeds, sitemap, rss, atom, jsonfeed, static_assets, redirects |
| Cleanup | Post-build tasks | pagefind |
Glob Stage ¶ #
glob ¶ #
Name: glob
Stage: Glob
Purpose: Discovers content files using configurable glob patterns with gitignore support.
Configuration (TOML):
[markata]
glob_patterns = ["**/*.md"] # Patterns to match
[markata-go.glob]
use_gitignore = true # Respect .gitignore patterns
Behavior:
- Matches files against configured glob patterns (supports
**recursive matching) - Excludes files matching
.gitignorepatterns if enabled - Deduplicates files matched by multiple patterns
- Returns sorted list for deterministic builds
Example:
[markata]
glob_patterns = [
"posts/**/*.md",
"pages/*.md",
"docs/**/*.md"
]
[markata-go.glob]
use_gitignore = true
Load Stage ¶ #
load ¶ #
Name: load
Stage: Load
Purpose: Reads discovered files and parses them into Post objects with frontmatter and content.
Configuration: None (uses content_dir from main config)
Behavior:
- Reads each file as UTF-8
- Extracts YAML frontmatter between
---delimiters - Creates Post object with parsed metadata and content
- Generates slug from frontmatter, title, or filename
- Generates href as
/{slug}/
Post fields set:
| Field | Type | Description |
|---|---|---|
path |
string | Source file path |
content |
string | Raw markdown content (after frontmatter) |
slug |
string | URL-friendly identifier |
href |
string | URL path (/{slug}/) |
title |
*string | From frontmatter |
date |
*time.Time | From frontmatter |
published |
bool | From frontmatter (default: false) |
draft |
bool | From frontmatter (default: false) |
skip |
bool | From frontmatter (default: false) |
tags |
[]string | From frontmatter |
description |
*string | From frontmatter |
template |
string | Template name from frontmatter |
Supported date formats:
- RFC3339:
2024-01-15T10:30:00Z - ISO datetime:
2024-01-15T10:30:00 - ISO datetime with space:
2024-01-15 10:30:00 - ISO date:
2024-01-15 - US format:
01/15/2024 - European format:
15-01-2024 - Long format:
January 15, 2024 - Short format:
Jan 15, 2024
frontmatter ¶ #
Name: frontmatter
Stage: Load (utility)
Purpose: Parses YAML frontmatter from markdown files. Used internally by the load plugin.
Behavior:
- Extracts content between
---delimiters - Parses YAML into a map
- Handles edge cases: no frontmatter, empty frontmatter, unclosed delimiters
Example frontmatter:
---
title: My Post
date: 2024-01-15
published: true
tags:
- go
- programming
description: A short description
template: post.html
custom_field: custom value
---
Transform Stage ¶ #
auto_title ¶ #
Name: auto_title
Stage: Transform (first priority)
Purpose: Auto-generates human-readable titles for posts that don’t have one by deriving them from filenames.
Configuration: None required.
Behavior:
- Runs with highest priority in Transform stage (before other transform plugins)
- Skips posts that already have a title set
- Extracts the filename without extension
- Replaces hyphens and underscores with spaces
- Applies title case (first letter of each word capitalized)
- Sets
post.Title
Post fields set:
| Field | Type | Description |
|---|---|---|
title |
*string | Generated title (if not already set) |
Example transformations:
| Filename | Generated Title |
|---|---|
my-first-post.md |
“My First Post” |
getting_started_guide.md |
“Getting Started Guide” |
hello-world.md |
“Hello World” |
2024-01-15-release-notes.md |
“2024 01 15 Release Notes” |
When to use:
- For quick drafts where you want titles auto-derived
- When filenames already describe the content well
- For sites with many posts where manual titling is tedious
Limitations:
- Does not extract titles from H1 headings in content (only uses filename)
- Numeric prefixes in filenames (like dates) become part of the title
- Special characters are kept as-is (not removed or transformed)
description ¶ #
Name: description
Stage: Transform
Purpose: Auto-generates meta descriptions for posts that don’t have one by extracting the first paragraph.
Configuration (TOML):
[markata]
description_max_length = 160 # Maximum characters (default: 160)
Behavior:
- Skips posts that already have a description
- Extracts first paragraph from content
- Strips markdown formatting (links, images, code, emphasis, headers, HTML)
- Truncates at word boundary with ellipsis
- Sets
post.Description
Post fields set:
| Field | Type | Description |
|---|---|---|
description |
*string | Generated description (if not already set) |
Example:
---
title: My Post
# No description - will be auto-generated
---
This is my first paragraph that will become the description.
It continues here but will be truncated at 160 characters...
## Second Section
More content here.
reading_time ¶ #
Name: reading_time
Stage: Transform
Purpose: Calculates word count and estimated reading time for each post.
Configuration (TOML):
[markata]
words_per_minute = 200 # Average reading speed (default: 200)
Behavior:
- Counts words in content (excludes code blocks, URLs, HTML)
- Calculates reading time based on words per minute
- Rounds up to nearest minute (minimum 1 minute)
- Stores results in post’s Extra map
Post fields added (in Extra):
| Field | Type | Description |
|---|---|---|
word_count |
int | Number of words |
reading_time |
int | Minutes to read |
reading_time_text |
string | Formatted string (e.g., “5 min read”) |
Template usage:
<span class="reading-time">{{ post.Extra.reading_time_text }}</span>
<span class="word-count">{{ post.Extra.word_count }} words</span>
stats ¶ #
Name: stats
Stage: Transform, Collect
Purpose: Calculates comprehensive content statistics for posts and aggregates them at feed and site levels.
Configuration (TOML):
[markata-go.stats]
words_per_minute = 200 # Average reading speed (default: 200)
include_code_in_count = false # Include code blocks in word count (default: false)
track_code_blocks = true # Count lines of code in code blocks (default: true)
Behavior:
- Transform stage: Calculates per-post statistics
- Word count (excluding code blocks by default)
- Character count (letters and digits only)
- Reading time estimate
- Code block count and lines of code
- Collect stage: Aggregates statistics
- Per-feed totals and averages
- Site-wide totals and averages
Post fields added (in Extra):
| Field | Type | Description |
|---|---|---|
word_count |
int | Number of words |
char_count |
int | Number of characters (no whitespace) |
reading_time |
int | Minutes to read |
reading_time_text |
string | Formatted (e.g., “5 min read”) |
code_lines |
int | Lines of code in code blocks |
code_blocks |
int | Number of code blocks |
stats |
PostStats | All stats as structured object |
Feed statistics (via cache):
| Field | Type | Description |
|---|---|---|
post_count |
int | Number of posts in feed |
total_words |
int | Sum of word counts |
total_reading_time |
int | Sum of reading times |
total_reading_time_text |
string | Formatted (e.g., “2 hours 30 min”) |
average_words |
int | Average word count |
average_reading_time |
int | Average reading time |
total_code_lines |
int | Total lines of code |
total_code_blocks |
int | Total code blocks |
Site statistics (in config.Extra.site_stats):
Same fields as feed statistics, aggregated across all posts.
Template usage:
{# Post-level stats #}
<div class="post-meta">
<span>{{ post.Extra.reading_time_text }}</span>
<span>{{ post.Extra.word_count }} words</span>
{% if post.Extra.code_blocks > 0 %}
<span>{{ post.Extra.code_lines }} lines of code</span>
{% endif %}
</div>
{# Site-level stats #}
<div class="site-stats">
<p>{{ config.Extra.site_stats.total_posts }} posts</p>
<p>{{ config.Extra.site_stats.total_reading_time_text }} total reading time</p>
</div>
Use cases:
- Display “X min read” on post cards
- Show word count for documentation pages
- Aggregate stats for feed index pages (“25 tutorials, ~8 hours”)
- Track content creation metrics
- Display code-heavy post indicators
breadcrumbs ¶ #
Name: breadcrumbs
Stage: Transform
Purpose: Generates breadcrumb navigation trails for posts based on URL path structure with JSON-LD structured data for SEO.
Configuration (TOML):
[markata-go.breadcrumbs]
enabled = true # Enable/disable breadcrumbs globally
show_home = true # Include "Home" as first breadcrumb
home_label = "Home" # Label for home breadcrumb
separator = "/" # Visual separator between breadcrumbs
max_depth = 0 # Maximum depth (0 = unlimited)
structured_data = true # Generate JSON-LD for SEO
# Alternative: under components section
[markata-go.components.breadcrumbs]
enabled = true
show_home = true
Frontmatter options:
---
title: "My Page"
# Disable breadcrumbs for this page
breadcrumbs: false
# Or customize per-page
breadcrumbs:
enabled: true
show_home: false
home_label: "Docs"
# Manual breadcrumb trail (overrides auto-generation)
items:
- label: "Products"
url: "/products/"
- label: "Widgets"
url: "/products/widgets/"
---
Post fields set:
| Field | Type | Description |
|---|---|---|
post.breadcrumbs |
[]Breadcrumb |
Array of breadcrumb items |
post.breadcrumb_separator |
string |
Configured separator character |
post.breadcrumbs_jsonld |
string |
JSON-LD structured data |
Breadcrumb item structure:
type Breadcrumb struct {
Label string // Display text
URL string // Link href
IsCurrent bool // True for last item
Position int // 1-indexed position
}
Auto-generation behavior:
- Parses post’s
hrefpath (e.g.,/docs/guides/getting-started/) - Creates breadcrumb for each path segment
- Humanizes segment names (e.g.,
getting-started→ “Getting Started”) - Uses post title for final segment if available
- Optionally prepends “Home” breadcrumb
Template usage:
{# Include the breadcrumbs component #}
{% include "components/breadcrumbs.html" %}
{# Or manual rendering #}
{% if post.breadcrumbs %}
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
{% for crumb in post.breadcrumbs %}
<li>
{% if crumb.is_current %}
<span aria-current="page">{{ crumb.label }}</span>
{% else %}
<a href="{{ crumb.url }}">{{ crumb.label }}</a>
{% endif %}
</li>
{% if not loop.last %}
<span class="separator">{{ post.breadcrumb_separator }}</span>
{% endif %}
{% endfor %}
</ol>
</nav>
{% endif %}
{# Add JSON-LD to head for SEO #}
{% if post.breadcrumbs_jsonld %}
<script type="application/ld+json">
{{ post.breadcrumbs_jsonld | safe }}
</script>
{% endif %}
Generated JSON-LD example:
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Docs",
"item": "https://example.com/docs/"
},
{
"@type": "ListItem",
"position": 3,
"name": "Getting Started"
}
]
}
Use cases:
- Documentation sites with deep hierarchies
- Multi-section websites (products, services, about)
- Blog categories and subcategories
- SEO improvement via structured data
- Improved user navigation and orientation
jinja_md ¶ #
Name: jinja_md
Stage: Transform
Purpose: Processes Jinja2 template expressions within markdown content before rendering.
Configuration: Requires jinja: true in post frontmatter.
Activation: Posts must explicitly enable Jinja processing:
---
title: Dynamic Post
jinja: true
---
Template context:
| Variable | Type | Description |
|---|---|---|
post |
Post | Current post object |
config |
Config | Site configuration |
posts |
[]Post | All posts |
core |
Manager | Lifecycle manager for filtering |
filter |
func | Filter posts by expression |
map |
func | Map field values from posts |
Example content:
---
title: Recent Posts Index
jinja: true
---
# Latest Posts
{% for p in filter("published == true")[:5] %}
- [{{ p.Title }}]({{ p.Href }}) - {{ p.Date.Format "Jan 2, 2006" }}
{% endfor %}
## All Tags
{% for tag in map("tags", "published == true", "date", true) %}
- {{ tag }}
{% endfor %}
wikilinks ¶ #
Name: wikilinks
Stage: Transform
Purpose: Transforms [[slug]] and [[slug|text]] wikilink syntax into HTML anchor tags.
Configuration (TOML):
[markata]
wikilinks_warn_broken = true # Warn about broken links (default: true)
Syntax:
Link to another post: [[other-post-slug]]
With custom text: [[other-post-slug|Click here to read more]]
Behavior:
- Finds all
[[...]]patterns in content - Looks up target post by slug (case-insensitive)
- If found: converts to
<a href="/slug/">Title</a> - If not found: keeps original syntax, adds warning to post
Post fields added (in Extra):
| Field | Type | Description |
|---|---|---|
wikilink_warnings |
[]string | List of broken wikilink warnings |
Example:
Check out my [[getting-started]] guide.
You might also like [[advanced-topics|the advanced guide]].
Becomes:
Check out my <a href="/getting-started/">Getting Started</a> guide.
You might also like <a href="/advanced-topics/">the advanced guide</a>.
toc ¶ #
Name: toc
Stage: Transform
Purpose: Extracts headings from markdown and builds a hierarchical table of contents.
Configuration (TOML):
[markata]
toc_min_level = 2 # Minimum heading level (default: 2, h2)
toc_max_level = 4 # Maximum heading level (default: 4, h4)
Behavior:
- Parses ATX-style headings (
# Heading) - Generates URL-safe IDs from heading text
- Handles duplicate IDs by appending numbers
- Builds hierarchical structure
Post fields added (in Extra):
| Field | Type | Description |
|---|---|---|
toc |
[]TocEntry | Hierarchical table of contents |
TocEntry structure:
type TocEntry struct {
Level int // Heading level (2-6)
Text string // Heading text
ID string // Anchor ID
Children []*TocEntry // Nested headings
}
Template usage:
{% if post.Extra.toc %}
<nav class="toc">
<h2>Table of Contents</h2>
<ul>
{% for entry in post.Extra.toc %}
<li>
<a href="#{{ entry.ID }}">{{ entry.Text }}</a>
{% if entry.Children %}
<ul>
{% for child in entry.Children %}
<li><a href="#{{ child.ID }}">{{ child.Text }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
structured_data ¶ #
Name: structured_data
Stage: Transform
Purpose: Generates JSON-LD Schema.org markup, OpenGraph meta tags, and Twitter Cards for SEO and social media optimization.
Configuration (TOML):
[seo]
twitter_handle = "yourusername" # Twitter/X handle (without @)
default_image = "/images/default-og.jpg" # Fallback social image
logo_url = "/images/logo.png" # Site logo for Schema.org
[seo.structured_data]
enabled = true # Enable/disable (default: true)
[seo.structured_data.default_author]
type = "Person"
name = "Author Name"
url = "https://example.com/about"
[seo.structured_data.publisher]
type = "Organization"
name = "Site Name"
url = "https://example.com"
logo = "/images/logo.png"
Behavior:
- Runs during Transform stage for each published post
- Skips posts without titles (required for structured data)
- Generates JSON-LD Schema.org BlogPosting markup
- Generates OpenGraph meta tags for social sharing
- Generates Twitter Card meta tags
- Stores all data in
post.Extra["structured_data"]
Post fields set (in Extra):
| Field | Type | Description |
|---|---|---|
structured_data |
*StructuredData | Container for all SEO metadata |
StructuredData contains:
JSONLD- JSON-LD script content (string)OpenGraph- Array of{Property, Content}pairsTwitter- Array of{Name, Content}pairs
Frontmatter fields used:
---
title: "Post Title" # Required
description: "Description" # For meta description
date: 2024-01-15 # Publication date
author: "Author Name" # Override default author
image: "/images/post.jpg" # Featured image
social_image: "/images/og.jpg" # Override for OG image
twitter: "authorhandle" # Author's Twitter (without @)
modified: "2024-01-16" # Last modified date
tags: ["tag1", "tag2"] # Become keywords
---
Example JSON-LD Output:
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "My Blog Post",
"description": "A description of my post",
"url": "https://example.com/my-post/",
"datePublished": "2024-01-15T00:00:00Z",
"dateModified": "2024-01-16T00:00:00Z",
"author": {
"@type": "Person",
"name": "Author Name",
"url": "https://example.com/about"
},
"publisher": {
"@type": "Organization",
"name": "Site Name",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/images/logo.png"
}
},
"image": "https://example.com/images/post.jpg",
"keywords": ["tag1", "tag2"]
}
Example OpenGraph Tags:
<meta property="og:title" content="My Blog Post">
<meta property="og:description" content="A description of my post">
<meta property="og:type" content="article">
<meta property="og:url" content="https://example.com/my-post/">
<meta property="og:site_name" content="Site Name">
<meta property="og:image" content="https://example.com/images/post.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:locale" content="en_US">
<meta property="article:published_time" content="2024-01-15T00:00:00Z">
<meta property="article:tag" content="tag1">
<meta property="article:tag" content="tag2">
Example Twitter Card Tags:
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@yourusername">
<meta name="twitter:creator" content="@authorhandle">
<meta name="twitter:title" content="My Blog Post">
<meta name="twitter:description" content="A description of my post">
<meta name="twitter:image" content="https://example.com/images/post.jpg">
Template usage:
{# JSON-LD in head #}
{% if post.structured_data.jsonld %}
<script type="application/ld+json">
{{ post.structured_data.jsonld | safe }}
</script>
{% endif %}
{# OpenGraph tags #}
{% for meta in post.structured_data.opengraph %}
<meta property="{{ meta.property }}" content="{{ meta.content }}">
{% endfor %}
{# Twitter Card tags #}
{% for meta in post.structured_data.twitter %}
<meta name="{{ meta.name }}" content="{{ meta.content }}">
{% endfor %}
Image priority:
social_imagefrom frontmatter (OG-specific override)imagefrom frontmatterdefault_imagefrom SEO config
Author priority:
authorfield from frontmatterdefault_authorfrom structured data configauthorfrom site config
Disabling structured data:
[seo.structured_data]
enabled = false
Testing tools:
Related:
- [[structured-data-guide|Structured Data Guide]] - Detailed user guide
- templates - Template integration
authors ¶ #
Name: authors
Stage: Transform
Priority: PriorityFirst + 1 (runs right after auto_title)
Purpose: Resolves author IDs in post frontmatter against the site-wide authors configuration, populating rich author objects for templates.
Configuration:
Authors are defined in the site configuration under [markata-go.authors]:
[markata-go.authors]
generate_pages = false
feeds_enabled = false
[markata-go.authors.authors.waylon]
name = "Waylon Walker"
role = "author"
active = true
default = true
[markata-go.authors.authors.guest]
name = "Guest Writer"
role = "editor"
guest = true
active = true
Behavior:
- Reads the authors map from the site configuration
- Identifies the default author (the entry with
default = true) - For each post:
- Reads author IDs from
authorsarray orauthorstring in frontmatter - If no authors specified, assigns the default author
- Resolves each author ID against the config map
- Applies per-post role overrides (from extended frontmatter format)
- Applies per-post details overrides (shown as hover tooltips in byline)
- Populates
post.AuthorObjectswith resolved Author structs
- Reads author IDs from
- Logs a warning for unrecognized author IDs
Post fields set:
| Field | Type | Description |
|---|---|---|
author_objects |
[]Author | Resolved author structs (computed, not serialized) |
authors |
[]string | Author IDs (may be set if default author assigned) |
Template access:
post.author_objects– Array of author maps with all fields (name, role, avatar, url, details, etc.)post.authors– Array of author ID stringsauthors– Top-level map of all configured authors (ID to author map)
Each author object includes a details field (when set via frontmatter) that provides a per-post description of the author’s contribution, displayed as a CSS tooltip on hover.
The extended frontmatter format supports key aliases for convenience (name/handle for id, job/position/part/title for role, detail/description for details). See the Frontmatter Guide for the full alias table.
Related:
- Authors Configuration – Config setup guide
- Frontmatter Guide –
authorsandauthorfields
Render Stage ¶ #
admonitions ¶ #
Name: admonitions
Stage: Render (goldmark extension)
Purpose: Renders admonition blocks (notes, warnings, tips) from markdown.
Syntax:
!!! note "Optional Title"
Content of the note goes here.
Must be indented with 4 spaces.
!!! warning
Default title will be "Warning"
!!! tip "Pro Tip"
Helpful information here.
Supported types:
note- General informationwarning- Caution/warningtip- Helpful tipsimportant- Important informationdanger- Dangerous/critical warningscaution- Proceed with care
HTML output:
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>Content of the note goes here.</p>
</div>
CSS styling:
.admonition {
padding: 1rem;
margin: 1rem 0;
border-left: 4px solid;
border-radius: 4px;
}
.admonition.note { border-color: #448aff; background: #e3f2fd; }
.admonition.warning { border-color: #ff9800; background: #fff3e0; }
.admonition.tip { border-color: #4caf50; background: #e8f5e9; }
.admonition.danger { border-color: #f44336; background: #ffebee; }
.admonition-title { font-weight: bold; margin-bottom: 0.5rem; }
render_markdown ¶ #
Name: render_markdown
Stage: Render
Purpose: Converts markdown content to HTML using goldmark with extensions.
Configuration (TOML):
[markdown.extensions]
typographer = true # Smart quotes, dashes, ellipses (default: true)
definition_list = true # PHP Markdown Extra definition lists (default: true)
footnote = true # PHP Markdown Extra footnotes (default: true)
Enabled extensions:
- GFM - GitHub Flavored Markdown (tables, strikethrough, autolinks, task lists)
- Syntax Highlighting - Code block highlighting with Chroma
- Admonitions - Note/warning blocks
- Auto Heading IDs - Generates IDs for headings
- Typographer - Smart quotes, dashes, ellipses
- Definition Lists - PHP Markdown Extra style definition lists
- Footnotes - PHP Markdown Extra style footnotes
Behavior:
- Takes
post.Content(raw markdown) - Converts to HTML with all extensions
- Stores result in
post.ArticleHTML
Post fields set:
| Field | Type | Description |
|---|---|---|
article_html |
string | Rendered HTML content |
Supported markdown features:
## Tables
| Header | Header |
|--------|--------|
| Cell | Cell |
## Task Lists
- [x] Completed task
- [ ] Pending task
## Strikethrough
~~deleted text~~
## Code Blocks
```go
func main() {
fmt.Println("Syntax highlighted!")
}
Footnotes ¶ #
Here’s a sentence with a footnote.1
Smart Quotes ¶ #
“Straight quotes” become curly.
Definition Lists ¶ #
- Term
- Definition of the term.
**Disabling extensions:**
All extensions are enabled by default. To disable any:
```toml
# Disable specific extensions
[markdown.extensions]
typographer = false # Use straight quotes instead
definition_list = false # Disable definition lists
footnote = false # Disable footnotes
# Or disable all three
[markdown.extensions]
typographer = false
definition_list = false
footnote = false
templates ¶ #
Name: templates
Stage: Render
Purpose: Wraps rendered markdown content in HTML templates with theme support.
Configuration (TOML):
[markata]
templates_dir = "templates" # Templates directory (default: "templates")
theme = "default" # Theme name (default: "default")
# Or with theme options:
[markata-go.theme]
name = "default"
Behavior:
- Looks up template from
post.Template(default:post.html) - Renders template with post context
- Falls back to raw
ArticleHTMLif no template exists - Stores result in
post.HTML
Template context:
| Variable | Type | Description |
|---|---|---|
post |
Post | Current post |
body |
string | post.ArticleHTML |
config |
Config | Site configuration |
core |
Manager | Lifecycle manager |
Post fields set:
| Field | Type | Description |
|---|---|---|
html |
string | Final HTML (content + template) |
Example template (templates/post.html):
<!DOCTYPE html>
<html>
<head>
<title>{{ post.Title }} | {{ config.Title }}</title>
<meta name="description" content="{{ post.Description }}">
</head>
<body>
<article>
<h1>{{ post.Title }}</h1>
<time>{{ post.Date.Format "January 2, 2006" }}</time>
<div class="content">{{ body | safe }}</div>
</article>
</body>
</html>
heading_anchors ¶ #
Name: heading_anchors
Stage: Render (post_render)
Purpose: Adds clickable anchor links to headings in rendered HTML for easy linking to sections.
Configuration (TOML):
[markata-go.heading_anchors]
enabled = true # Enable/disable the plugin (default: true)
min_level = 2 # Minimum heading level to process (default: 2, h2)
max_level = 4 # Maximum heading level to process (default: 4, h4)
position = "end" # Anchor position: "start" or "end" (default: "end")
symbol = "#" # Link text for the anchor (default: "#")
class = "heading-anchor" # CSS class for the anchor link (default: "heading-anchor")
Behavior:
- Runs after
render_markdownto processArticleHTML - Finds all heading tags within the configured level range
- Extracts existing IDs or generates URL-safe IDs from heading text
- Handles duplicate IDs by appending numbers (e.g.,
my-heading,my-heading-1) - Inserts anchor link at the configured position (start or end of heading)
HTML output example:
<!-- Input (from render_markdown) -->
<h2 id="getting-started">Getting Started</h2>
<!-- Output (with default config, position="end") -->
<h2 id="getting-started">Getting Started <a href="#getting-started" class="heading-anchor">#</a></h2>
<!-- Output (with position="start") -->
<h2 id="getting-started"><a href="#getting-started" class="heading-anchor">#</a> Getting Started</h2>
CSS styling:
.heading-anchor {
opacity: 0;
text-decoration: none;
margin-left: 0.5rem;
color: #666;
}
h2:hover .heading-anchor,
h3:hover .heading-anchor,
h4:hover .heading-anchor {
opacity: 1;
}
md_video ¶ #
Name: md_video
Stage: Render (post_render)
Purpose: Converts markdown image syntax for video files into HTML video elements with GIF-like autoplay behavior by default.
Configuration (TOML):
[markata-go.md_video]
enabled = true # Enable the plugin (default: true)
video_extensions = [".mp4", ".webm", ".ogg", ".ogv", ".mov", ".m4v"] # Extensions to treat as video
video_class = "md-video" # CSS class for video elements (default)
controls = true # Show video controls (default: true)
autoplay = true # Auto-start playback (default: true)
loop = true # Loop video continuously (default: true)
muted = true # Mute audio (default: true, required for autoplay)
playsinline = true # Play inline on mobile (default: true)
preload = "metadata" # Preload hint: "none", "metadata", "auto" (default: "metadata")
Why GIF-like defaults?
The default configuration mimics animated GIF behavior because most embedded videos in blog posts are short demonstrations, screen recordings, or animations. Users expect these to play automatically without sound, similar to GIFs.
To use traditional video behavior (click to play with sound):
[markata-go.md_video]
autoplay = false
loop = false
muted = false
controls = true
Markdown usage:



Behavior:
- Scans
ArticleHTMLfor<img>tags after markdown rendering - Checks if the
srcattribute ends with a video extension (handles query parameters) - Replaces matching
<img>tags with<video>elements - Preserves the
alttext as fallback content - Automatically detects MIME type from file extension
HTML output:
<video autoplay loop muted playsinline controls preload="metadata" class="md-video">
<source src="https://example.com/video.mp4" type="video/mp4">
kickflip down the 3 stair - fingerboarding
</video>
MIME type detection:
| Extension | MIME Type |
|---|---|
.mp4 |
video/mp4 |
.webm |
video/webm |
.ogg, .ogv |
video/ogg |
.mov |
video/quicktime |
.m4v |
video/x-m4v |
.avi |
video/x-msvideo |
CSS styling:
.md-video {
max-width: 100%;
height: auto;
border-radius: 4px;
}
youtube ¶ #
Name: youtube
Stage: Render (post_render)
Purpose: Converts YouTube URLs on their own line into responsive embedded iframes with privacy-enhanced mode by default.
Configuration (TOML):
[markata-go.youtube]
enabled = true # Enable the plugin (default: true)
privacy_enhanced = true # Use youtube-nocookie.com (default: true)
container_class = "youtube-embed" # CSS class for container (default)
lazy_load = true # Enable lazy loading (default: true)
Markdown usage:
Simply paste a YouTube URL on its own line:
Check out this video:
https://www.youtube.com/watch?v=dQw4w9WgXcQ
More content below...
Supported URL formats:
https://www.youtube.com/watch?v=VIDEO_IDhttps://youtube.com/watch?v=VIDEO_IDhttps://m.youtube.com/watch?v=VIDEO_IDhttps://youtu.be/VIDEO_ID- URLs with extra parameters like
&t=30(timestamp is ignored in embed)
HTML output:
<div class="youtube-embed">
<iframe
src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
loading="lazy">
</iframe>
</div>
Privacy-enhanced mode:
By default, the plugin uses youtube-nocookie.com which prevents YouTube from storing cookies on visitors’ browsers until they play the video. Disable this for standard embeds:
[markata-go.youtube]
privacy_enhanced = false
CSS styling (included in default theme):
.youtube-embed {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
max-width: 100%;
margin: 1.5rem 0;
border-radius: 0.5rem;
}
.youtube-embed iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
border-radius: 0.5rem;
}
Behavior:
- Scans
ArticleHTMLfor YouTube URLs in standalone<p>tags - URLs inside code blocks or inline with text are NOT converted
- Extracts the 11-character video ID from the URL
- Generates responsive embed with configurable privacy mode
- Adds lazy loading by default for better page performance
Notes:
- URLs must be on their own line (in their own
<p>tag) to be converted - Inline URLs like “Check out https://youtube.com/watch?v=xyz" are preserved as text
- Invalid video IDs (not exactly 11 characters) are ignored
image_zoom ¶ #
Name: image_zoom
Stage: Render (post_render), Write
Purpose: Adds optional image zoom/lightbox functionality using GLightbox. Users can click/tap images to view them in a full-screen modal with support for touch gestures, keyboard navigation, and gallery mode.
Configuration (TOML):
[markata-go.image_zoom]
enabled = false # Enable the plugin (default: false)
library = "glightbox" # Lightbox library to use (default: "glightbox")
selector = ".glightbox" # CSS selector for zoomable images (default: ".glightbox")
cdn = true # Use CDN for library files (default: true)
auto_all_images = false # Make all images zoomable by default (default: false)
# GLightbox-specific options
open_effect = "zoom" # Effect when opening: "zoom", "fade", "none" (default: "zoom")
close_effect = "zoom" # Effect when closing: "zoom", "fade", "none" (default: "zoom")
slide_effect = "slide" # Effect when sliding: "slide", "fade", "zoom", "none" (default: "slide")
touch_navigation = true # Enable touch/swipe gestures (default: true)
loop = false # Loop through images in gallery (default: false)
draggable = true # Enable dragging images to navigate (default: true)
Markdown syntax:
Mark individual images as zoomable using attribute markers:



Frontmatter option:
Enable zoom for all images in a specific post:
---
title: "Photo Gallery"
image_zoom: true
---
Behavior:
- Runs after
render_markdownto processArticleHTML - Finds images with
{data-zoomable}or{.zoomable}markers - Removes the markers from alt text
- Wraps images in anchor tags with GLightbox classes
- Adds
data-glightboxattribute for the lightbox - Tracks which posts need the lightbox library
- In Write stage, stores configuration for templates to inject JS/CSS
HTML output:
Input:

Output:
<a href="/images/sunset.jpg" class="glightbox-link">
<img src="/images/sunset.jpg" alt="Beautiful sunset"
class="glightbox" data-glightbox="description: Beautiful sunset">
</a>
Post fields set (in Extra):
| Field | Type | Description |
|---|---|---|
needs_image_zoom |
bool | True if post has zoomable images |
Config fields set (in Extra):
| Field | Type | Description |
|---|---|---|
glightbox_enabled |
bool | True if any posts need GLightbox |
glightbox_cdn |
bool | Whether to use CDN |
glightbox_options |
map | GLightbox initialization options |
Template usage:
The base template automatically includes GLightbox when needed:
{% if config.Extra.glightbox_enabled %}
<!-- CSS in head -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/glightbox.min.css">
<!-- JS before closing body -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/glightbox.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
GLightbox({ selector: '.glightbox' });
});
</script>
{% endif %}
Keyboard shortcuts:
| Key | Action |
|---|---|
Escape |
Close lightbox |
→ / ArrowRight |
Next image |
← / ArrowLeft |
Previous image |
Touch gestures:
| Gesture | Action |
|---|---|
| Swipe left/right | Navigate images |
| Pinch | Zoom in/out |
| Drag | Pan zoomed image |
image_optimization ¶ #
Name: image_optimization
Stage: Render (post_render), Write
Purpose: Generates AVIF/WebP variants for local images and rewrites HTML to use <picture> with stable fallbacks.
Configuration (TOML):
[markata-go.image_optimization]
enabled = true
formats = ["avif", "webp"]
quality = 80
avif_quality = 80
webp_quality = 80
widths = [480, 768, 1200]
sizes = "100vw"
cache_dir = ".markata/image-cache"
avifenc_path = ""
cwebp_path = ""
Configuration fields:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Whether the plugin is active |
formats |
[]string | ["avif","webp"] |
Formats to generate |
quality |
int | 80 |
Default quality for all formats |
avif_quality |
int | 80 |
AVIF quality override |
webp_quality |
int | 80 |
WebP quality override |
widths |
[]int | [480,768,1200] |
Responsive widths to generate |
sizes |
string | "100vw" |
Sizes attribute for responsive sources |
cache_dir |
string | .markata/image-cache |
Cache directory for encode metadata |
avifenc_path |
string | "" |
Path to avifenc (auto-detect if empty) |
cwebp_path |
string | "" |
Path to cwebp (auto-detect if empty) |
Behavior:
- Scans
ArticleHTMLfor local<img>tags and skips external URLs or data URIs. - Wraps each local image in
<picture>with AVIF/WebP sources andsrcsetwidths. - Writes optimized variants next to the original output file using width suffixes.
- Uses a cache key (path, mod time, size, quality, encoder) to skip re-encoding.
- Missing encoders emit warnings and skip the format without failing the build.
HTML output:
Input:

Output:
<picture>
<source type="image/avif" srcset="/images/cat-480w.avif 480w, /images/cat-768w.avif 768w" sizes="100vw">
<source type="image/webp" srcset="/images/cat-480w.webp 480w, /images/cat-768w.webp 768w" sizes="100vw">
<img src="/images/cat.jpg" alt="Cat">
</picture>
| Tap outside | Close lightbox |
CSS customization:
.glightbox-link {
display: inline-block;
cursor: zoom-in;
}
.glightbox-link:hover img {
opacity: 0.9;
transform: scale(1.02);
transition: all 0.2s ease;
}
Performance notes:
- GLightbox JS/CSS only loaded on pages with zoomable images
- Library is ~11KB gzipped when using CDN
- Files loaded asynchronously to avoid blocking page render
link_collector ¶ #
Name: link_collector
Stage: Render (post_render)
Purpose: Collects all hyperlinks from posts and tracks inlinks (pages linking TO a post) and outlinks (pages a post links TO).
Configuration (TOML):
[markata-go.link_collector]
include_feeds = false # Include feed pages in inlinks (default: false)
include_index = false # Include index page in inlinks (default: false)
Note: The plugin also reads url from config.Extra to determine internal vs external links.
Behavior:
- Runs after
render_markdownto processArticleHTML - Extracts all
<a href="...">links from each post - Resolves relative URLs against the post’s URL
- Distinguishes internal links (same domain) from external links
- Looks up target posts for internal links
- Populates
InlinksandOutlinkson each post - Stores all links in cache for use by other plugins
Performance:
- Caches href extraction per post using a hash of
ArticleHTML - Reuses cached hrefs on unchanged posts, while still rebuilding link objects
Post fields set:
| Field | Type | Description |
|---|---|---|
hrefs |
[]string | All href values found in the post |
inlinks |
[]map | Links from other posts pointing to this post (template-friendly) |
outlinks |
[]map | Links from this post to other pages (template-friendly) |
Link structure:
type Link struct {
SourceURL string // Full URL of the source page
SourcePost *Post // Source post object (if internal)
TargetPost *Post // Target post object (if internal)
RawTarget string // Original href value
TargetURL string // Resolved full URL
TargetDomain string // Domain of target URL
IsInternal bool // True if same domain
IsSelf bool // True if self-link
SourceText string // Link text from source
TargetText string // Title of target post (if internal)
}
Template usage:
{% if post.inlinks %}
<aside class="backlinks">
<h3>Pages that link here</h3>
<ul>
{% for link in post.inlinks %}
<li><a href="{{ link.source_post.href }}">{{ link.source_post.title }}</a></li>
{% endfor %}
</ul>
</aside>
{% endif %}
{% if post.outlinks %}
<aside class="outlinks">
<h3>Links from this page</h3>
<ul>
{% for link in post.outlinks %}
<li>
<a href="{{ link.target_url }}">
{% if link.is_internal %}{{ link.target_text }}{% else %}{{ link.target_domain }}{% endif %}
</a>
{% if not link.is_internal %}<span class="external">↗</span>{% endif %}
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
chartjs ¶ #
Name: chartjs
Stage: Render (after render_markdown)
Purpose: Converts chartjs code blocks into interactive Chart.js charts.
Status: Enabled by default. Set enabled = false to disable.
Configuration (TOML):
[markata-go.chartjs]
enabled = true # Enabled by default; set to false to disable
cdn_url = "/assets/vendor/chartjs/chart.min.js" # Chart.js URL (local by default)
container_class = "chartjs-container" # CSS class for wrapper div
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable the plugin |
cdn_url |
/assets/vendor/chartjs/chart.min.js |
Chart.js URL (local by default) |
container_class |
chartjs-container |
CSS class for wrapper div |
Markdown syntax:
```chartjs
{
"type": "bar",
"data": {
"labels": ["Red", "Blue", "Yellow"],
"datasets": [{
"label": "My Dataset",
"data": [12, 19, 3],
"backgroundColor": ["#ff6384", "#36a2eb", "#ffce56"]
}]
}
}
```
HTML output:
<div class="chartjs-container">
<canvas id="chart-abc123"></canvas>
<script>
(function() {
var ctx = document.getElementById('chart-abc123').getContext('2d');
new Chart(ctx, {"type":"bar","data":{...}});
})();
</script>
</div>
Supported chart types:
bar,line,pie,doughnut,radar,polarArea,bubble,scatter
Template requirements: Include Chart.js in your base template:
<script src="/assets/vendor/chartjs/chart.min.js"></script>
Examples ¶ #
Bar Chart - Monthly Sales Data ¶ #
A simple bar chart showing monthly sales performance:
Line Chart - Website Traffic Over Time ¶ #
Track trends with a line chart showing page views and unique visitors:
Pie Chart - Content Distribution ¶ #
Visualize proportions with a pie chart showing content by category:
Doughnut Chart with Center Text ¶ #
A doughnut chart is useful for showing a primary metric with breakdown:
contribution_graph ¶ #
Name: contribution_graph
Stage: Render (after render_markdown)
Purpose: Renders GitHub-style calendar heatmaps showing activity over time using Cal-Heatmap.
Configuration (TOML):
[markata-go.contribution_graph]
enabled = true
cdn_url = "/assets/vendor/cal-heatmap" # Cal-Heatmap base URL (local by default)
container_class = "contribution-graph-container"
theme = "light"
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable the plugin |
cdn_url |
/assets/vendor/cal-heatmap |
Cal-Heatmap base URL (local by default) |
container_class |
contribution-graph-container |
CSS class for wrapper div |
theme |
light |
Color theme (light, dark) |
Markdown syntax:
```contribution-graph
{
"data": [
{"date": "2024-01-01", "value": 5},
{"date": "2024-01-02", "value": 3},
{"date": "2024-01-03", "value": 8}
],
"options": {
"domain": "year",
"subDomain": "day",
"cellSize": 12
}
}
```
HTML output:
<div class="contribution-graph-container">
<div id="contribution-graph-1"></div>
</div>
<link rel="stylesheet" href="/assets/vendor/cal-heatmap/cal-heatmap.css">
<script src="/assets/vendor/cal-heatmap/cal-heatmap.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const cal = new CalHeatmap();
cal.paint({
itemSelector: '#contribution-graph-1',
data: { source: [...], x: 'date', y: 'value' },
domain: { type: 'year' },
subDomain: { type: 'day' }
});
});
</script>
Data format:
date: ISO date string (YYYY-MM-DD)value: Numeric value (affects cell color intensity)
Options:
| Option | Type | Default | Description |
|---|---|---|---|
domain |
string | year |
Time domain: year, month, week, day |
subDomain |
string | day |
Sub-domain: day, hour, minute |
cellSize |
number | 10 |
Cell size in pixels |
range |
number | 1 |
Number of domain units to display |
Use cases:
- Blog post publishing frequency
- Contribution tracking
- Habit tracking displays
Related: Analytics Guide
one_line_link ¶ #
Name: one_line_link
Stage: Render (after render_markdown)
Purpose: Expands standalone URLs in paragraphs into styled link preview cards.
Configuration (TOML):
[markata-go.one_line_link]
enabled = true
card_class = "link-card"
fallback_title = "Link"
timeout = 5
exclude_patterns = ["^https://twitter\\.com", "^https://x\\.com"]
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable the plugin |
card_class |
link-card |
CSS class for the link card |
fallback_title |
Link |
Title when metadata unavailable |
timeout |
5 |
HTTP timeout in seconds |
exclude_patterns |
[] |
Regex patterns for URLs to skip |
Behavior:
- Finds paragraphs containing only a URL (
<p>https://...</p>) - Extracts the domain from the URL
- Converts to a styled link card
Before:
<p>https://example.com/article</p>
After:
<a href="https://example.com/article" class="link-card" target="_blank" rel="noopener noreferrer">
<div class="link-card-content">
<div class="link-card-title">Link</div>
<div class="link-card-url">example.com</div>
</div>
</a>
CSS example:
.link-card {
display: block;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 8px;
text-decoration: none;
transition: box-shadow 0.2s;
}
.link-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.link-card-title {
font-weight: bold;
color: var(--color-text);
}
.link-card-url {
font-size: 0.875rem;
color: var(--color-text-muted);
}
wikilink_hover ¶ #
Name: wikilink_hover
Stage: Render (after wikilinks)
Purpose: Adds hover preview data attributes to wikilinks for tooltip/popup previews.
Status: Enabled by default. Set enabled = false to disable.
Configuration (TOML):
[markata-go.wikilink_hover]
enabled = true # Enabled by default; set to false to disable
preview_length = 200 # Max characters for preview text
include_image = true # Add preview image if available
screenshot_service = "" # Optional: "https://screenshot.example.com/capture?url="
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable the plugin |
preview_length |
200 |
Max characters for preview text |
include_image |
true |
Add preview image if available |
screenshot_service |
"" |
URL prefix for screenshot service |
Behavior:
- Finds
<a class="wikilink">tags created by the wikilinks plugin - Looks up the target post by href
- Adds data attributes for hover previews
Data attributes added:
| Attribute | Description |
|---|---|
data-preview |
Truncated description or content |
data-preview-image |
Featured image URL (if available) |
data-preview-screenshot |
Screenshot service URL (if configured) |
Before:
<a href="/my-post/" class="wikilink">My Post</a>
After:
<a href="/my-post/" class="wikilink"
data-preview="This is a description of the post..."
data-preview-image="/images/featured.jpg">My Post</a>
JavaScript for hover previews:
document.querySelectorAll('.wikilink[data-preview]').forEach(link => {
link.addEventListener('mouseenter', (e) => {
const preview = e.target.dataset.preview;
const image = e.target.dataset.previewImage;
// Show tooltip with preview content and optional image
});
});
Image field lookup order: The plugin checks these Extra fields for images:
imagefeatured_imagecover_imageog_imagethumbnail
Examples ¶ #
Basic Wikilink with Hover Preview ¶ #
When you link to another post using wikilink syntax, the hover plugin automatically adds preview data:
Markdown input:
Learn more about [[getting-started|getting started with markata-go]].
For advanced users, check out [[plugin-development]].
Rendered HTML (with hover data):
<a href="/getting-started/" class="wikilink"
data-preview="A comprehensive guide to installing and configuring markata-go for your first static site project."
data-preview-image="/images/getting-started-hero.jpg">getting started with markata-go</a>
<a href="/plugin-development/" class="wikilink"
data-preview="Learn how to extend markata-go with custom plugins that hook into the build lifecycle.">Plugin Development</a>
Implementing Hover Previews with JavaScript ¶ #
Add this JavaScript to enable hover preview tooltips:
// Simple tooltip implementation for wikilink hover previews
document.addEventListener('DOMContentLoaded', () => {
const tooltip = document.createElement('div');
tooltip.className = 'wikilink-tooltip';
tooltip.style.cssText = `
position: absolute;
max-width: 300px;
padding: 12px;
background: var(--color-surface, #fff);
border: 1px solid var(--color-border, #ddd);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-size: 0.875rem;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
`;
document.body.appendChild(tooltip);
document.querySelectorAll('.wikilink[data-preview]').forEach(link => {
link.addEventListener('mouseenter', (e) => {
const preview = e.target.dataset.preview;
const image = e.target.dataset.previewImage;
let content = '';
if (image) {
content += `<img src="${image}" style="width:100%;border-radius:4px;margin-bottom:8px;">`;
}
content += `<p style="margin:0">${preview}</p>`;
tooltip.innerHTML = content;
tooltip.style.opacity = '1';
const rect = e.target.getBoundingClientRect();
tooltip.style.left = `${rect.left + window.scrollX}px`;
tooltip.style.top = `${rect.bottom + window.scrollY + 8}px`;
});
link.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
});
});
});
CSS Styling for Wikilinks ¶ #
Style your wikilinks to indicate they have preview functionality:
/* Base wikilink styling */
.wikilink {
color: var(--color-primary);
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 2px;
cursor: pointer;
}
/* Indicate preview availability */
.wikilink[data-preview] {
text-decoration-style: dashed;
}
.wikilink[data-preview]:hover {
text-decoration-style: solid;
background: var(--color-primary-light, rgba(0, 120, 212, 0.1));
border-radius: 2px;
}
/* Tooltip styling */
.wikilink-tooltip {
line-height: 1.5;
color: var(--color-text);
}
.wikilink-tooltip img {
display: block;
max-height: 150px;
object-fit: cover;
}
Using Screenshot Service for External Previews ¶ #
Configure a screenshot service for richer previews:
[markata-go.wikilink_hover]
enabled = true
preview_length = 200
include_image = true
screenshot_service = "https://api.microlink.io/?screenshot=true&url="
This adds a data-preview-screenshot attribute for external screenshot generation:
<a href="/complex-diagrams/" class="wikilink"
data-preview="Learn to create complex diagrams with Mermaid.js"
data-preview-screenshot="https://api.microlink.io/?screenshot=true&url=https://mysite.com/complex-diagrams/">
Complex Diagrams
</a>
encryption ¶ #
Name: encryption
Stage: Render (priority 50 – after markdown rendering, before templates)
Purpose: Encrypts content for private posts using AES-256-GCM client-side encryption.
Status: Enabled by default with default_key = "default".
Configuration (TOML):
[encryption]
enabled = true
default_key = "default"
decryption_hint = "Contact me for access"
[encryption.private_tags]
diary = "personal"
Behavior:
- Applies
private_tagsconfig – posts with matching tags are markedprivate: true - Validates that every private post has an available encryption key
- If any private post has no key, returns a CriticalError that halts the build
- Encrypts
ArticleHTMLof each private post with the resolved key - Replaces content with an encrypted wrapper containing a password prompt
Encryption keys are loaded from environment variables:
MARKATA_GO_ENCRYPTION_KEY_DEFAULT=password
MARKATA_GO_ENCRYPTION_KEY_PERSONAL=another-password
Post fields read:
Private– whether the post is privateSecretKey– which encryption key to use (set viasecret_key,private_key, orencryption_keyfrontmatter)Tags– checked againstprivate_tagsconfigArticleHTML– the content to encrypt
Post fields set:
ArticleHTML– replaced with encrypted wrapper HTMLExtra["has_encrypted_content"]–true(for template script inclusion)Extra["encryption_key_name"]– the key name used
Related: See the Encryption Guide for complete usage documentation.
Collect Stage ¶ #
overwrite_check ¶ #
Name: overwrite_check
Stage: Collect (early priority)
Purpose: Detects when multiple posts or feeds would write to the same output path, preventing accidental content overwrites and catching slug conflicts early in the build.
Configuration: No user-facing configuration required. The plugin runs automatically.
Behavior:
- Runs early in the Collect stage (before feeds plugin)
- Checks all post output paths (
{output_dir}/{slug}/index.html) - Checks all feed output paths (HTML, RSS, Atom, JSON)
- Detects conflicts where multiple sources would write to the same file
- Fails the build with detailed error messages
Error Output Example:
detected 2 output path conflict(s):
- markout/about/index.html: post:content/about.md, feed:about
- markout/blog/index.html: post:content/blog.md, post:content/blog-page.md
Common Conflict Scenarios:
- Post and feed with same slug:
# content/about.md
---
slug: "about"
---
# markata-go.toml - conflicts!
[[markata-go.feeds]]
slug = "about"
- Two posts with same slug:
# content/post-a.md
---
slug: "my-page"
---
# content/post-b.md
---
slug: "my-page" # Conflict!
---
Resolution strategies:
- Rename one of the conflicting slugs
- Use different paths for feeds vs content
- Remove duplicate content
Files checked for conflicts:
| Source | Output Path |
|---|---|
| Post | {output_dir}/{slug}/index.html |
| Feed HTML | {output_dir}/{feed_slug}/index.html |
| Feed RSS | {output_dir}/{feed_slug}/rss.xml |
| Feed Atom | {output_dir}/{feed_slug}/atom.xml |
| Feed JSON | {output_dir}/{feed_slug}/index.json |
static_file_conflicts ¶ #
Name: static_file_conflicts
Stage: Collect (late priority)
Purpose: Detects when static files in the static/ directory would silently clobber generated content, causing unexpected behavior like private posts not being added to robots.txt.
Configuration (TOML):
[markata-go.static_file_conflicts]
enabled = true # Enable/disable the lint rule (default: true)
static_dir = "static" # Static files directory (default: "static")
Behavior:
- Runs late in the Collect stage (after content is processed)
- Scans the
static/directory for files - Checks for posts that would generate root-level files (robots.md → robots.txt)
- Checks for feed output paths (rss.xml, atom.xml, sitemap.xml)
- Reports warnings when conflicts are detected
- Continues the build (non-blocking warning)
Warning Output Example:
WARNING: 2 static file conflict(s) detected
Static files will override generated content.
Conflicting /robots.txt:
Generated: pages/robots.md → /robots.txt
Static: static/robots.txt → /robots.txt
The static file will override the generated one.
Conflicting /sitemap.xml:
Generated: sitemap → /sitemap.xml
Static: static/sitemap.xml → /sitemap.xml
The static file will override the generated one.
To fix:
- Remove the static file if you want the generated version
- Remove or rename the source file if you want the static version
- Disable the static_file_conflicts lint rule in config if intentional
Common Conflict Scenarios:
-
robots.txt conflict:
pages/robots.md → generates /robots.txt (with private posts excluded) static/robots.txt → copied to /robots.txt (overwrites generated)Impact: Private posts may NOT be added to robots.txt Disallow rules.
-
sitemap.xml conflict:
Sitemap plugin → generates /sitemap.xml static/sitemap.xml → overwrites with static versionImpact: New posts won’t appear in the sitemap for search engines.
-
Feed conflicts:
RSS plugin → generates /rss.xml static/rss.xml → overwrites with static version
Checked file patterns:
| Generated Source | Checks For Static File |
|---|---|
robots.md |
static/robots.txt |
sitemap.md |
static/sitemap.xml |
humans.md |
static/humans.txt |
security.md |
static/security.txt |
manifest.md |
static/manifest.json, static/manifest.webmanifest |
| RSS feed | static/rss.xml, static/feed.xml |
| Atom feed | static/atom.xml |
| JSON feed | static/index.json, static/feed.json |
Disabling the lint rule: If the conflict is intentional (you specifically want the static file to override):
[markata-go.static_file_conflicts]
enabled = false
Related:
- [[#overwrite_check|overwrite_check]] - Detects conflicts between generated files
- [[#static_assets|static_assets]] - Copies static files to output
series ¶ #
Name: series
Stage: Collect (PriorityEarly)
Purpose: Scans posts for series frontmatter and auto-generates series feed configs with prev/next navigation for sequential reading.
Posts declare series membership with series: "series-name" in frontmatter. The plugin groups posts by series, sorts them (by series_order or date ascending), sets prev/next navigation, and injects series FeedConfig entries so the feeds plugin renders them.
Frontmatter:
---
title: "Part 1: Getting Started"
series: "Building a CLI in Go"
series_order: 1 # optional, defaults to date-ascending order
---
Configuration (TOML):
[markata-go.series]
slug_prefix = "series" # URL prefix (default: "series")
auto_sidebar = true # Auto-enable feed sidebar (default: true)
[markata-go.series.defaults]
items_per_page = 0 # No pagination by default
sidebar = true # Show sidebar on series posts
[markata-go.series.defaults.formats]
html = true
rss = true
atom = false
json = false
# Per-series overrides (keyed by series name)
[markata-go.series.overrides."building-a-cli-in-go"]
title = "Building a CLI in Go"
description = "A step-by-step guide to building CLIs with Go and Cobra"
Behavior:
- Posts can belong to only one series
- Series feeds are created at
/{slug_prefix}/{series-slug}/(e.g.,/series/building-a-cli-in-go/) - Posts within a series get Prev/Next navigation and position indicators (“Part X of Y”)
- Series feeds automatically show in the feed sidebar, taking precedence over other feeds
- Feed type is set to
FeedTypeSeries - Runs before the
feedsplugin so generated configs are available for processing
Related:
- [[series-guide|Series Guide]] - Complete usage documentation
- feeds - Processes the feed configs generated by this plugin
- [[#prevnext|prevnext]] - Calculates prev/next from feeds and series
feeds ¶ #
Name: feeds
Stage: Collect
Purpose: Processes configured feed definitions, filtering, sorting, and paginating posts.
Configuration (TOML):
[[markata-go.feeds]]
slug = "blog"
title = "Blog Posts"
description = "All blog posts"
filter = "published == true and not draft"
sort = "date"
reverse = true
items_per_page = 10
[markata-go.feeds.formats]
html = true
rss = true
atom = false
json = false
[[markata-go.feeds]]
slug = "tutorials"
title = "Tutorials"
filter = "tags contains 'tutorial'"
sort = "date"
reverse = true
Feed configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
slug |
string | required | URL path for the feed |
title |
string | "” | Feed title |
description |
string | "" | Feed description |
filter |
string | "" | Filter expression |
sort |
string | “date” | Sort field |
reverse |
bool | true | Reverse sort order |
items_per_page |
int | 10 | Posts per page |
formats |
object | varies | Output formats |
Filter expression syntax:
# Equality
filter = "published == true"
filter = "category == 'news'"
# Contains (for arrays)
filter = "tags contains 'go'"
# Boolean combinations
filter = "published == true and not draft"
filter = "tags contains 'go' or tags contains 'python'"
# Comparisons
filter = "date >= '2024-01-01'"
See [[feeds-guide|Feeds Guide]] for complete filter syntax.
auto_feeds ¶ #
Name: auto_feeds
Stage: Collect
Purpose: Automatically generates feeds for tags, categories, and date archives.
Configuration (TOML):
[markata-go.auto_feeds.tags]
enabled = true
slug_prefix = "tags" # Results in /tags/python/, /tags/go/, etc.
[markata-go.auto_feeds.tags.formats]
html = true
rss = true
[markata-go.auto_feeds.categories]
enabled = true
slug_prefix = "categories"
[markata-go.auto_feeds.categories.formats]
html = true
rss = true
[markata-go.auto_feeds.archives]
enabled = true
slug_prefix = "archive"
yearly_feeds = true # /archive/2024/
monthly_feeds = false # /archive/2024/01/
[markata-go.auto_feeds.archives.formats]
html = true
rss = false
Generated feeds:
For tags:
/tags/python/- Posts tagged “python”/tags/go/- Posts tagged “go”
For categories (uses category field from post Extra):
/categories/tutorials/- Posts in tutorials category
For archives:
/archive/2024/- All posts from 2024/archive/2024/01/- All posts from January 2024 (if monthly enabled)
prevnext ¶ #
Name: prevnext
Stage: Collect
Purpose: Calculates previous/next post links for navigation based on feed ordering.
Configuration (TOML):
[markata-go.prevnext]
enabled = true # Enable/disable the plugin (default: true)
strategy = "first_feed" # Strategy for determining navigation context
default_feed = "blog" # Default feed slug (for "explicit_feed" strategy)
Strategy options:
| Strategy | Description |
|---|---|
first_feed |
Use the first feed that contains the post (default) |
explicit_feed |
Always use the feed specified in default_feed |
series |
Use series frontmatter field, fall back to first_feed |
frontmatter |
Use prevnext_feed frontmatter field, fall back to first_feed |
Behavior:
- Runs after the
feedsplugin to access feed data - For each post, determines which feed to use based on strategy
- Finds the post’s position within that feed
- Sets
PrevandNextpost references - Sets
PrevNextContextwith additional navigation metadata
Post fields set:
| Field | Type | Description |
|---|---|---|
prev |
*Post | Previous post in sequence (nil if first) |
next |
*Post | Next post in sequence (nil if last) |
prev_next_feed |
string | Feed slug used for navigation |
prev_next_context |
*PrevNextContext | Full navigation context |
PrevNextContext structure:
type PrevNextContext struct {
FeedSlug string // Slug of the feed used
FeedTitle string // Title of the feed
Position int // 1-indexed position in feed
Total int // Total posts in feed
Prev *Post // Previous post
Next *Post // Next post
}
Template usage:
{% if post.Prev or post.Next %}
<nav class="post-navigation">
{% if post.Prev %}
<a href="{{ post.Prev.Href }}" class="nav-prev">
<span class="nav-label">Previous</span>
<span class="nav-title">{{ post.Prev.Title }}</span>
</a>
{% endif %}
{% if post.Next %}
<a href="{{ post.Next.Href }}" class="nav-next">
<span class="nav-label">Next</span>
<span class="nav-title">{{ post.Next.Title }}</span>
</a>
{% endif %}
</nav>
{% endif %}
<!-- With context -->
{% if post.PrevNextContext %}
<div class="series-progress">
Post {{ post.PrevNextContext.Position }} of {{ post.PrevNextContext.Total }}
in {{ post.PrevNextContext.FeedTitle }}
</div>
{% endif %}
Using series for tutorials:
---
title: "Part 2: Advanced Topics"
series: "go-tutorial"
---
[markata-go.prevnext]
strategy = "series"
Write Stage ¶ #
publish_html ¶ #
Name: publish_html
Stage: Write
Purpose: Writes individual post HTML files to the output directory.
Configuration: Uses output_dir from main config.
Behavior:
- Skips posts marked as skip, unpublished, or draft
- Creates directory:
{output_dir}/{slug}/ - Writes
index.htmlwith post’s HTML content - Uses
post.HTMLif available, otherwise wrapsArticleHTMLin basic template
Output structure:
output/
getting-started/
index.html
advanced-topics/
index.html
my-first-post/
index.html
random_post ¶ #
Name: random_post
Stage: Write
Purpose: Writes a static /random/ page that picks a random eligible post in the browser.
Configuration (TOML):
[markata-go.random_post]
enabled = true
path = "random" # default: "random"
exclude_tags = ["draft"] # optional
emit_posts_json = false # optional
Outputs:
/{path}/index.html/{path}/posts.json(optional)
Eligibility (defaults): published, not draft/private/skip, and not tagged with any exclude_tags.
well_known ¶ #
Name: well_known
Stage: Write
Purpose: Generates .well-known endpoints from site metadata.
Configuration: markata-go.well_known
Behavior:
- Resolves enabled entries from
auto_generate - Renders templates in
templates/well-known/if present - Writes files into
output/.well-known/
Generated files (defaults):
/.well-known/host-meta/.well-known/host-meta.json/.well-known/webfinger/.well-known/nodeinfoand/nodeinfo/2.0/.well-known/time
Optional files:
/.well-known/sshfp(whenssh_fingerprintis set)/.well-known/keybase.txt(whenkeybase_usernameis set)
publish_feeds ¶ #
Name: publish_feeds
Stage: Write
Purpose: Writes feed output files in all configured formats (HTML, RSS, Atom, JSON, Markdown, Text).
Behavior: For each feed, generates enabled formats:
| Format | File | Description |
|---|---|---|
| HTML | index.html, page/2/index.html |
Paginated HTML pages |
| RSS | rss.xml |
RSS 2.0 feed |
| Atom | atom.xml |
Atom 1.0 feed |
| JSON | feed.json |
JSON Feed 1.1 |
| Markdown | slug.md |
Markdown listing |
| Text | slug.txt |
Plain text listing |
Output structure:
output/
blog/
index.html # Page 1
page/
2/
index.html # Page 2
3/
index.html # Page 3
rss.xml
atom.xml
feed.json
tags/
python/
index.html
rss.xml
sitemap ¶ #
Name: sitemap
Stage: Write
Purpose: Generates a sitemap.xml file listing all published posts and feed pages.
Configuration: None (uses url from main config for absolute URLs)
Behavior:
- Adds home page with highest priority
- Adds all published posts with their dates
- Adds feed index pages
- Writes to
{output_dir}/sitemap.xml
Output example:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://example.com/my-post/</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
rss ¶ #
Name: rss
Stage: Write (utility)
Purpose: Generates RSS 2.0 feeds. Used by publish_feeds.
Output format: RSS 2.0 with Atom namespace for self-reference.
Example output:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>My Blog</title>
<link>https://example.com</link>
<description>A blog about things</description>
<language>en-us</language>
<lastBuildDate>Mon, 15 Jan 2024 10:00:00 +0000</lastBuildDate>
<atom:link href="https://example.com/blog/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<title>My First Post</title>
<link>https://example.com/my-first-post/</link>
<description>Post description or excerpt...</description>
<pubDate>Mon, 15 Jan 2024 10:00:00 +0000</pubDate>
<guid isPermaLink="true">https://example.com/my-first-post/</guid>
</item>
</channel>
</rss>
atom ¶ #
Name: atom
Stage: Write (utility)
Purpose: Generates Atom 1.0 feeds. Used by publish_feeds.
Output format: Atom 1.0
Example output:
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>My Blog</title>
<id>https://example.com/blog/atom.xml</id>
<updated>2024-01-15T10:00:00Z</updated>
<link href="https://example.com" rel="alternate" type="text/html"/>
<link href="https://example.com/blog/atom.xml" rel="self" type="application/atom+xml"/>
<author>
<name>Author Name</name>
</author>
<entry>
<title>My First Post</title>
<id>https://example.com/my-first-post/</id>
<updated>2024-01-15T10:00:00Z</updated>
<published>2024-01-15T10:00:00Z</published>
<link href="https://example.com/my-first-post/" rel="alternate" type="text/html"/>
<summary type="text">Post description</summary>
<content type="html">Full HTML content...</content>
</entry>
</feed>
jsonfeed ¶ #
Name: jsonfeed
Stage: Write (utility)
Purpose: Generates JSON Feed 1.1 documents. Used by publish_feeds.
Output format: JSON Feed 1.1
Example output:
{
"version": "https://jsonfeed.org/version/1.1",
"title": "My Blog",
"home_page_url": "https://example.com",
"feed_url": "https://example.com/blog/feed.json",
"description": "A blog about things",
"language": "en",
"authors": [
{ "name": "Author Name" }
],
"items": [
{
"id": "https://example.com/my-first-post/",
"url": "https://example.com/my-first-post/",
"title": "My First Post",
"content_html": "<p>Full HTML content...</p>",
"content_text": "Plain text content...",
"summary": "Post description",
"date_published": "2024-01-15T10:00:00Z",
"tags": ["go", "programming"]
}
]
}
static_assets ¶ #
Name: static_assets
Stage: Write
Purpose: Copies static assets from theme and project directories to output.
Configuration (TOML):
[markata]
theme = "default" # Theme name for theme static files
Behavior:
- Copies theme static files from
themes/{theme}/static/ - Copies project static files from
static/ - Project files override theme files (local customization)
Directory structure:
mysite/
themes/
default/
static/
css/
theme.css
js/
theme.js
static/
css/
custom.css # Overrides or adds to theme
images/
logo.png
output/
css/
theme.css # From theme
custom.css # From project
js/
theme.js # From theme
images/
logo.png # From project
palette_css ¶ #
Name: palette_css
Stage: Write (after static_assets)
Purpose: Generates CSS custom properties from the configured color palette, enabling theme customization without modifying CSS files directly.
Configuration (TOML):
[markata-go.theme]
palette = "nord" # Palette name (built-in or custom)
Built-in palettes:
catppuccin-latte- Light pastel themecatppuccin-mocha- Dark pastel themedracula- Dark purple/pink themegruvbox-dark- Dark retro groove themegruvbox-light- Light retro groove themenord- Arctic, north-bluish color paletteone-dark- Atom One Dark inspiredrose-pine- All-natural pine, faux fur, and soho vibessolarized-dark- Precision dark themesolarized-light- Precision light themetokyo-night- Dark theme inspired by Tokyo night lights
Behavior:
- Reads palette name from
config.Extra["theme"]["palette"] - Loads the palette definition (built-in or from
palettes/directory) - Maps palette semantic colors to theme CSS variable names
- Generates
css/variables.csswith CSS custom properties - Includes default font families, spacing, and layout values
Generated CSS structure:
/* CSS Custom Properties - Nord Theme */
:root {
--color-primary: #88c0d0;
--color-primary-light: #8fbcbb;
--color-primary-dark: #8fbcbb;
/* Semantic colors */
--color-text: #eceff4;
--color-text-muted: #d8dee9;
--color-background: #2e3440;
--color-surface: #3b4252;
--color-border: #4c566a;
/* Status colors */
--color-success: #a3be8c;
--color-warning: #ebcb8b;
--color-error: #bf616a;
--color-info: #81a1c1;
/* Font families */
--font-body: system-ui, -apple-system, ...;
--font-heading: var(--font-body);
--font-mono: ui-monospace, ...;
/* Font sizes, spacing, layout... */
}
Custom palettes:
Create a TOML file in the palettes/ directory:
# palettes/my-palette.toml
name = "My Custom Palette"
type = "dark" # or "light"
[colors]
accent = "#ff6b6b"
accent-hover = "#ee5a5a"
text-primary = "#f8f8f2"
text-muted = "#b0b0b0"
bg-primary = "#1a1a2e"
bg-surface = "#16213e"
border = "#2d3a5a"
success = "#50fa7b"
warning = "#f1fa8c"
error = "#ff5555"
info = "#8be9fd"
Template usage: Variables are automatically available in your CSS:
.my-component {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.button-primary {
background: var(--color-primary);
color: var(--color-background);
}
Why use palettes?
- Switch themes by changing one config value
- Consistent colors across your entire site
- Built-in palettes tested for accessibility
- Easy to create and share custom themes
chroma_css ¶ #
Name: chroma_css
Stage: Configure + Write
Purpose: Generates CSS for syntax highlighting from Chroma themes. Creates css/chroma.css with syntax highlighting styles that work with render_markdown’s CSS-class-based highlighting.
Configuration (TOML):
# Explicit theme setting
[markdown.highlight]
theme = "github-dark" # Chroma theme name
Or auto-derived from palette:
[theme]
palette = "catppuccin-mocha" # Automatically selects matching Chroma theme
Behavior:
- During Configure: Reads theme from
markdown.highlight.themeor derives from palette - Falls back to variant-appropriate default (dark palettes -> github-dark, light palettes -> github)
- During Write: Generates
{output_dir}/css/chroma.csswith syntax highlighting styles
Available Chroma Themes:
Light themes:
github,gruvbox-light,solarized-light,catppuccin-latterose-pine-dawn,xcode,vs,friendly,tango
Dark themes:
github-dark,gruvbox,solarized-dark,catppuccin-mochadracula,monokai,nord,onedark,rose-pinetokyo-night,vim
Example CSS Output:
/* Syntax highlighting - generated from Chroma theme: github-dark */
.chroma { color: #e6edf3; background-color: #0d1117 }
.chroma .err { color: #f85149 }
.chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
.chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
.chroma .hl { background-color: #2d333b }
.chroma .ln { color: #6e7681; }
.chroma .k { color: #ff7b72 } /* Keyword */
.chroma .kc { color: #79c0ff } /* KeywordConstant */
.chroma .kd { color: #ff7b72 } /* KeywordDeclaration */
/* ... additional token styles ... */
Template usage: Include the generated CSS in your base template:
<link rel="stylesheet" href="/css/chroma.css">
Palette to Chroma Theme Mapping:
| Palette | Chroma Theme |
|---|---|
catppuccin-mocha |
catppuccin-mocha |
catppuccin-latte |
catppuccin-latte |
dracula |
dracula |
nord |
nord |
gruvbox-dark |
gruvbox |
gruvbox-light |
gruvbox-light |
solarized-dark |
solarized-dark |
solarized-light |
solarized-light |
rose-pine |
rose-pine |
tokyo-night |
tokyo-night |
Related plugins:
- [[#render_markdown|render_markdown]] - Uses CSS classes for syntax highlighting
- [[#palette_css|palette_css]] - Generates theme color variables
redirects ¶ #
Name: redirects
Stage: Write
Purpose: Generates HTML redirect pages from a _redirects file for URL migration and link preservation.
Configuration (TOML):
[markata-go.redirects]
redirects_file = "static/_redirects" # Path to redirects file (default)
redirect_template = "" # Custom template path (optional)
Redirects file format (static/_redirects):
# Comments start with #
# Format: /old-path /new-path
/old-post/ /new-post/
/blog/2020/hello/ /posts/hello-world/
/about-me/ /about/
Behavior:
- Reads the
_redirectsfile (skips silently if not found) - Parses redirect rules (ignores comments, empty lines, wildcards)
- For each rule, creates
{output_dir}/{old-path}/index.html - Uses HTML meta refresh and canonical link for SEO-friendly redirects
- Caches results to avoid regeneration on unchanged content
Supported redirect syntax:
- Simple redirects:
/old /new - Paths must start with
/ - Wildcards (
*) are ignored (not supported for static generation) - Status codes in the file are ignored (always uses meta refresh)
Generated HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Refresh" content="0; url='/new-path/'" />
<link rel="canonical" href="/new-path/" />
<meta name="description" content="/old-path/ has been moved to /new-path/." />
<title>/old-path/ has been moved to /new-path/</title>
</head>
<body>
<h1>Page Moved</h1>
<p><code>/old-path/</code> has moved to <a href="/new-path/">/new-path/</a></p>
</body>
</html>
Custom template: Create a custom template with these available variables:
<!-- templates/redirect.html -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Refresh" content="0; url='{{ .New }}'" />
<link rel="canonical" href="{{ .New }}" />
<title>Redirecting...</title>
</head>
<body>
<p>Redirecting from {{ .Original }} to <a href="{{ .New }}">{{ .New }}</a></p>
</body>
</html>
[markata-go.redirects]
redirect_template = "templates/redirect.html"
Template variables:
| Variable | Type | Description |
|---|---|---|
.Original |
string | Source path (e.g., /old-post/) |
.New |
string | Destination path (e.g., /new-post/) |
.Config |
*Config | Site configuration object |
qrcode ¶ #
Name: qrcode
Stage: Write
Purpose: Generates QR code images for each post’s URL, useful for print materials or sharing.
Configuration (TOML):
[markata-go.qrcode]
enabled = true
format = "svg" # "svg" or "png"
size = 200 # Size in pixels
output_dir = "qrcodes" # Subdirectory in output
error_correction = "M" # L, M, Q, H
foreground = "#000000" # QR code color
background = "#ffffff" # Background color
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable QR code generation |
format |
svg |
Output format: svg or png |
size |
200 |
Image size in pixels |
output_dir |
qrcodes |
Output subdirectory name |
error_correction |
M |
Error correction level |
foreground |
#000000 |
QR code foreground color |
background |
#ffffff |
QR code background color |
Error correction levels:
| Level | Recovery | Use Case |
|---|---|---|
L |
~7% | Clean environments |
M |
~15% | Standard use (default) |
Q |
~25% | Industrial/outdoor |
H |
~30% | Maximum durability |
Behavior:
- For each non-skipped post, builds the absolute URL
- Generates QR code in the configured format
- Saves to
{output_dir}/{qrcode_output_dir}/{slug}.{format} - Adds
qrcode_urlto post’s Extra map
Output structure:
output/
qrcodes/
hello-world.svg
my-first-post.svg
another-article.svg
Post field set:
| Field | Type | Description |
|---|---|---|
qrcode_url |
string | Relative URL to QR code image |
Template usage:
{% if post.qrcode_url %}
<div class="qr-code">
<img src="{{ post.qrcode_url }}" alt="QR Code for {{ post.Title }}" />
<p>Scan to visit this page</p>
</div>
{% endif %}
Print stylesheet example:
@media print {
.qr-code {
display: block;
page-break-inside: avoid;
text-align: center;
margin-top: 2rem;
}
.qr-code img {
width: 150px;
height: 150px;
}
}
@media screen {
.qr-code {
display: none; /* Hide on screen, show in print */
}
}
Custom colors example:
[markata-go.qrcode]
foreground = "#2e3440" # Nord dark
background = "#eceff4" # Nord light
Default Plugin Order ¶ #
When using DefaultPlugins(), plugins execute in this order:
[]lifecycle.Plugin{
// Glob stage
NewGlobPlugin(),
// Load stage
NewLoadPlugin(),
// Transform stage (in order)
NewAutoTitlePlugin(), // Auto-generate titles from filenames
NewDescriptionPlugin(), // Auto-generate descriptions early
NewReadingTimePlugin(), // Calculate reading time
NewStructuredDataPlugin(), // Generate SEO metadata
NewWikilinksPlugin(), // Process wikilinks
NewTocPlugin(), // Extract TOC
NewJinjaMdPlugin(), // Process Jinja templates
// Render stage
NewRenderMarkdownPlugin(),
NewHeadingAnchorsPlugin(), // Add anchor links to headings
NewChartJSPlugin(), // Convert Chart.js code blocks to charts
NewCSVFencePlugin(), // Convert CSV code blocks to tables
NewMermaidPlugin(), // Convert Mermaid code blocks to diagrams
NewGlossaryPlugin(), // Auto-link glossary terms
NewWikilinkHoverPlugin(), // Add hover data to wikilinks
NewLinkCollectorPlugin(), // Track inlinks/outlinks
NewTemplatesPlugin(),
// Collect stage
NewOverwriteCheckPlugin(), // Check for output path conflicts (early)
NewFeedsPlugin(),
NewAutoFeedsPlugin(),
NewPrevNextPlugin(), // Calculate prev/next navigation
// Write stage
NewStaticAssetsPlugin(), // Copy static assets first
NewChromaCSSPlugin(), // Generate syntax highlighting CSS
NewPublishFeedsPlugin(),
NewPublishHTMLPlugin(),
NewSitemapPlugin(),
NewRedirectsPlugin(), // Generate redirect pages
}
Optional Plugins ¶ #
The following plugins are not enabled by default and must be explicitly configured. They provide specialized functionality that may not be needed for all sites.
webmentions_fetch ¶ #
Name: webmentions_fetch
Stage: Transform
Priority: First (-200, runs before jinja_md)
Purpose: Fetches incoming webmentions from webmention.io and attaches them to posts.
Configuration (TOML):
[markata-go.webmentions]
enabled = true
webmention_io_token = "your_token_here" # Or use WEBMENTION_IO_TOKEN env var
cache_dir = ".cache/webmentions"
timeout = "30s"
user_agent = "markata-go/1.0 (WebMention)"
Behavior:
- Reads cached webmentions from
{cache_dir}/received_mentions.json - Groups mentions by target URL
- For each post, matches webmentions to the post’s URL (with various URL format normalizations)
- Attaches matched webmentions to
post.Extra["webmentions"]
Incremental builds: When build cache is enabled, the plugin skips re-attaching webmentions if no posts are scheduled to rebuild.
Fetching webmentions: The plugin loads from cache during builds. To fetch fresh webmentions from webmention.io:
markata-go webmentions fetch
This fetches all webmentions for your domain and saves them to the cache.
Post fields set (in Extra):
| Field | Type | Description |
|---|---|---|
webmentions |
[]ReceivedWebMention |
Array of webmentions for this post |
ReceivedWebMention structure:
type ReceivedWebMention struct {
Source string // Source URL
Target string // Target URL (your post)
WMProperty string // Type: "like-of", "repost-of", "in-reply-to", "bookmark-of", "mention-of"
Author Author // Author info (name, photo, url)
Content Content // Content (text, html)
Published time.Time // When the mention was published
}
Template usage:
{% if post.Extra.webmentions %}
<section class="webmentions">
{% set likes = post.Extra.webmentions|selectattr("WMProperty", "equalto", "like-of")|list %}
{% set reposts = post.Extra.webmentions|selectattr("WMProperty", "equalto", "repost-of")|list %}
{% set replies = post.Extra.webmentions|selectattr("WMProperty", "equalto", "in-reply-to")|list %}
{% if likes %}
<div class="likes">
<h4>{{ likes|length }} Likes</h4>
<div class="facepile">
{% for mention in likes %}
<a href="{{ mention.Author.URL }}" title="{{ mention.Author.Name }}">
<img src="{{ mention.Author.Photo }}" alt="{{ mention.Author.Name }}">
</a>
{% endfor %}
</div>
</div>
{% endif %}
</section>
{% endif %}
URL matching: The plugin tries multiple URL variants to match webmentions:
- Exact URL match
- With/without trailing slash
- Legacy
/blog/prefix (for migrated sites) - Double-slash normalization (common in webmention.io data)
Related:
- WebMentions Guide - Complete webmentions documentation
- [[#webmentions_leaderboard|webmentions_leaderboard]] - Engagement statistics
webmentions_leaderboard ¶ #
Name: webmentions_leaderboard
Stage: Transform
Priority: -150 (after webmentions_fetch, before jinja_md)
Purpose: Calculates top posts by webmention engagement and provides leaderboard data for templates.
Configuration: None required. Automatically processes webmentions attached by webmentions_fetch.
Behavior:
- Iterates through all posts with webmentions
- Counts mentions by type (likes, reposts, replies, bookmarks, mentions)
- Creates sorted leaderboards (top 20 posts each)
- Stores results in
config.Extra["webmention_leaderboard"]
Config fields set (in Extra):
| Field | Type | Description |
|---|---|---|
webmention_leaderboard |
WebmentionLeaderboard |
Leaderboard data structure |
WebmentionLeaderboard structure:
type WebmentionLeaderboard struct {
TopLiked []LeaderboardEntry // Top 20 by likes
TopReposted []LeaderboardEntry // Top 20 by reposts
TopReplied []LeaderboardEntry // Top 20 by replies
TopTotal []LeaderboardEntry // Top 20 by total engagement
TotalLikes int // Site-wide total likes
TotalReposts int // Site-wide total reposts
TotalReplies int // Site-wide total replies
TotalMentions int // Site-wide total mentions
}
type LeaderboardEntry struct {
Post *Post // Reference to the post
Href string // Post URL
Title string // Post title
Likes int // Number of likes
Reposts int // Number of reposts
Replies int // Number of replies
Bookmarks int // Number of bookmarks
Mentions int // Number of mentions
Total int // Total engagement
}
Template usage (in Jinja-enabled pages):
---
title: "Analytics"
jinja: true
---
## Webmention Leaderboard
{% if config.Extra.webmention_leaderboard %}
### Most Liked Posts
| Likes | Post |
|------:|------|
{% for entry in config.Extra.webmention_leaderboard.TopLiked %}| {{ entry.Likes }} | [{{ entry.Title }}]({{ entry.Href }}) |
{% endfor %}
### Most Discussed Posts
| Replies | Post |
|--------:|------|
{% for entry in config.Extra.webmention_leaderboard.TopReplied %}| {{ entry.Replies }} | [{{ entry.Title }}]({{ entry.Href }}) |
{% endfor %}
### Site Totals
- **Total Likes:** {{ config.Extra.webmention_leaderboard.TotalLikes }}
- **Total Reposts:** {{ config.Extra.webmention_leaderboard.TotalReposts }}
- **Total Replies:** {{ config.Extra.webmention_leaderboard.TotalReplies }}
{% endif %}
Use cases:
- Analytics dashboards showing top-performing content
- “Popular posts” widgets based on social engagement
- Content performance tracking
- Gamification/motivation for content creators
Related:
- WebMentions Guide - Complete webmentions documentation
- [[#webmentions_fetch|webmentions_fetch]] - Fetches webmentions
mermaid ¶ #
Name: mermaid
Stage: Render (post_render)
Purpose: Converts Mermaid code blocks into rendered diagrams. Supports client-side rendering (default), CLI-based pre-rendering via mmdc, or headless Chromium pre-rendering via mermaidcdp.
Status: Enabled by default. Set enabled = false to disable.
Configuration (TOML):
[markata-go.mermaid]
enabled = true # Enabled by default; set to false to disable
mode = "client" # "client", "cli", or "chromium"
cdn_url = "/assets/vendor/mermaid/mermaid.esm.min.mjs" # Mermaid URL (client mode, local by default)
theme = "default" # Mermaid theme (default, dark, forest, neutral)
use_css_variables = true # Derive diagram colors from site CSS palette (default: true)
lightbox = true # Click diagrams to open in lightbox with pan/zoom (default: true)
lightbox_selector = ".glightbox-mermaid" # CSS selector for lightbox links (default: ".glightbox-mermaid")
# CLI mode settings
[markata-go.mermaid.cli]
mmdc_path = "" # Auto-detect if empty
extra_args = "" # Additional mmdc arguments
# Chromium mode settings
[markata-go.mermaid.chromium]
browser_path = "" # Auto-detect if empty
timeout = 30 # Seconds per diagram
max_concurrent = 4 # Parallel diagram renderers
no_sandbox = false # Required in containers (Docker, etc.)
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Whether mermaid processing is active |
mode |
string | "client" |
Rendering mode: client (browser-side JS), cli (mmdc), or chromium (headless Chrome) |
cdn_url |
string | /assets/vendor/mermaid/mermaid.esm.min.mjs |
Mermaid URL (client mode, local by default) |
theme |
string | "default" |
Mermaid theme: default, dark, forest, neutral. Ignored when use_css_variables is true. |
use_css_variables |
bool | true |
Read site CSS custom properties (--color-background, --color-text, --color-primary, --color-code-bg, --color-surface) and pass them to Mermaid’s theming. Hardcoded fallbacks are used if variables are not defined. |
lightbox |
bool | true |
Enable click-to-zoom lightbox overlay with interactive pan and zoom via svg-pan-zoom. |
lightbox_selector |
string | ".glightbox-mermaid" |
CSS selector for mermaid lightbox links. Retained for backward compatibility; the programmatic GLightbox API does not use it. |
chromium.no_sandbox |
bool | false |
Disable Chromium sandbox. Required when running inside Docker, Distrobox, or other containerized environments. |
chromium.timeout |
int | 30 |
Maximum seconds to wait for a single diagram to render. |
chromium.max_concurrent |
int | 4 |
Maximum number of diagrams to render in parallel. |
Behavior:
- Finds code blocks with
language-mermaidclass inArticleHTML - In client mode: converts to
<pre class="mermaid">blocks and injects Mermaid.js initialization script - In cli/chromium modes: pre-renders diagrams to static SVGs embedded directly in HTML
- Only injects the script once per post (even with multiple diagrams)
- When
use_css_variablesis true, reads site CSS custom properties and passes them tomermaid.initialize()so diagrams match the site palette automatically - When
lightboxis true, attaches click handlers to each rendered SVG. Clicking opens a programmatic GLightbox overlay with svg-pan-zoom for interactive pan and zoom. svg-pan-zoom (~29KB) is lazy-loaded from vendor assets by default. - In chromium mode, the MermaidJS library is cached at
~/.cache/markata-go/mermaid/to avoid re-downloading on each build
Markdown usage:
```mermaid
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug]
D --> B
```
Supported diagram types:
- Flowcharts (
graph TD,graph LR) - Sequence diagrams
- Class diagrams
- State diagrams
- Entity Relationship diagrams
- Gantt charts
- Pie charts
- Git graphs
- And more (see Mermaid docs)
HTML output:
<pre class="mermaid">
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug]
D --> B
</pre>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'default' });
</script>
When lightbox is enabled, each rendered SVG also gets a click handler that opens a GLightbox overlay. The lightbox contains a toolbar with Fit / + / - controls and supports mouse wheel zoom and click-drag panning via svg-pan-zoom.
Lightbox / Pan-Zoom for Diagrams ¶ #
When lightbox = true (the default), clicking any rendered mermaid diagram opens a full-screen overlay with interactive pan and zoom.
How it works:
- A separate GLightbox instance (
selector: false) is created for mermaid diagrams to avoid conflicts with image lightbox - svg-pan-zoom v3.6.2 is lazy-loaded from CDN on first click (zero cost for pages without interaction)
- GLightbox
draggableis set tofalseto prevent conflict with svg-pan-zoom panning - On lightbox open,
requestAnimationFrameis used toresize()+fit()+center()the diagram for reliable initial positioning
Interaction:
| Action | Result |
|---|---|
| Click diagram | Open lightbox with full-size diagram |
| Mouse wheel / pinch | Zoom in / out |
| Click + drag | Pan the diagram |
| Fit button | Reset zoom to fit diagram in viewport |
| + / - buttons | Zoom in / out incrementally |
| Escape / click overlay | Close lightbox |
CSS classes:
| Class | Purpose |
|---|---|
.mermaid-lightbox-wrap |
Container inside lightbox (100% width, 90vh height, overflow hidden) |
.mermaid-lightbox-toolbar |
Absolute-positioned button toolbar (top-right) |
.mermaid-pz-btn |
Themed control buttons using var(--color-surface), var(--color-border), var(--color-text) |
.svg-pan-zoom_viewport |
Added by svg-pan-zoom; styled with cursor: grab / cursor: grabbing |
GLightbox theming:
The lightbox overlay is themed to match the site’s dark palette. CSS overrides in components.css apply to:
.goverlay– dark background overlay.ginlined-content– content container- Navigation buttons and description area
Performance:
- svg-pan-zoom JS (~29KB, BSD-2 license) is only loaded on first diagram click
- GLightbox CSS/JS is loaded with the page (shared with image_zoom if enabled)
- Pan-zoom instance is destroyed on lightbox close to prevent memory leaks
Examples ¶ #
Flowchart - Build Process ¶ #
Visualize processes and decision trees with flowcharts:
flowchart TD
A[Start Build] --> B{Config Found?}
B -->|Yes| C[Load Configuration]
B -->|No| D[Use Defaults]
C --> E[Discover Content]
D --> E
E --> F[Parse Frontmatter]
F --> G[Transform Content]
G --> H[Render Markdown]
H --> I[Apply Templates]
I --> J[Write Output]
J --> K[Generate Feeds]
K --> L{Search Enabled?}
L -->|Yes| M[Build Search Index]
L -->|No| N[Complete]
M --> N
Sequence Diagram - API Request Flow ¶ #
Show interactions between components over time:
sequenceDiagram
participant Browser
participant Server
participant Cache
participant Database
Browser->>Server: GET /api/posts
Server->>Cache: Check cache
alt Cache hit
Cache-->>Server: Return cached data
else Cache miss
Server->>Database: Query posts
Database-->>Server: Return posts
Server->>Cache: Store in cache
end
Server-->>Browser: JSON response
Note over Browser,Server: Response includes ETag header
Browser->>Server: GET /api/posts (with If-None-Match)
Server-->>Browser: 304 Not Modified
Class Diagram - Plugin Architecture ¶ #
Document code structure and relationships:
classDiagram
class Plugin {
<>
+Name() string
}
class ConfigurePlugin {
<>
+Configure(m *Manager) error
}
class TransformPlugin {
<>
+Transform(m *Manager) error
}
class RenderPlugin {
<>
+Render(m *Manager) error
}
class Manager {
-config *Config
-posts []*Post
-plugins []Plugin
+RunStage(stage Stage) error
+Posts() []*Post
+ProcessPostsConcurrently(fn func) error
}
class Post {
+Path string
+Title *string
+Content string
+ArticleHTML string
+Slug string
+Tags []string
}
Plugin <|-- ConfigurePlugin
Plugin <|-- TransformPlugin
Plugin <|-- RenderPlugin
Manager --> Plugin : uses
Manager --> Post : manages
State Diagram - Post Lifecycle ¶ #
Show state transitions for content:
stateDiagram-v2
[*] --> Draft: Create post
Draft --> Review: Submit for review
Draft --> Draft: Edit
Review --> Draft: Request changes
Review --> Scheduled: Approve
Scheduled --> Published: Publish date reached
Scheduled --> Draft: Cancel
Published --> Archived: Archive
Published --> Published: Update
Archived --> Published: Restore
Archived --> [*]: Delete
Entity Relationship Diagram - Content Model ¶ #
Document data relationships:
erDiagram
POST ||--o{ TAG : has
POST ||--o| AUTHOR : written_by
POST ||--o{ FEED : belongs_to
POST ||--o{ LINK : contains
FEED ||--o{ POST : contains
AUTHOR ||--o{ POST : writes
POST {
string slug PK
string title
string content
datetime date
boolean published
}
TAG {
string name PK
string slug
}
AUTHOR {
string id PK
string name
string email
}
FEED {
string slug PK
string title
string filter
}
LINK {
string source FK
string target
boolean internal
}
Gantt Chart - Project Timeline ¶ #
Plan and visualize project schedules:
gantt
title Site Redesign Project
dateFormat YYYY-MM-DD
section Planning
Requirements gathering :a1, 2024-01-01, 7d
Design mockups :a2, after a1, 10d
Technical planning :a3, after a1, 5d
section Development
Theme development :b1, after a2, 14d
Plugin customization :b2, after a3, 10d
Content migration :b3, after b1, 7d
section Testing
QA testing :c1, after b3, 5d
Performance testing :c2, after c1, 3d
section Launch
Soft launch :milestone, after c2, 0d
Full launch :d1, after c2, 2d
Git Graph - Branch History ¶ #
Visualize git workflows:
gitGraph
commit id: "Initial commit"
commit id: "Add base theme"
branch feature/blog
checkout feature/blog
commit id: "Add blog layout"
commit id: "Add post template"
checkout main
branch feature/search
checkout feature/search
commit id: "Add Pagefind"
checkout main
merge feature/blog id: "Merge blog feature"
checkout feature/search
commit id: "Style search UI"
checkout main
merge feature/search id: "Merge search"
commit id: "v1.0.0 release" tag: "v1.0.0"
glossary ¶ #
Name: glossary
Stage: Render (post_render) + Write
Purpose: Automatically links glossary terms in post content and exports a glossary JSON file.
Status: Enabled by default. Set enabled = false to disable.
Configuration (TOML):
[markata-go.glossary]
enabled = true # Enabled by default; set to false to disable
link_class = "glossary-term" # CSS class for glossary links (default)
case_sensitive = false # Case-sensitive term matching (default: false)
tooltip = true # Add title attribute with description (default: true)
max_links_per_term = 1 # Max times to link each term (0 = all, default: 1)
exclude_tags = ["glossary"] # Tags to exclude from linking (default: ["glossary"])
export_json = true # Export glossary.json file (default: true)
glossary_path = "glossary" # Path prefix for glossary posts (default)
template_key = "glossary" # Frontmatter key to identify glossary posts (default)
Glossary post format:
Create posts in the glossary/ directory (or set glossary_path):
---
title: "API"
description: "Application Programming Interface - a set of protocols for building software"
templateKey: glossary
aliases:
- "APIs"
- "Application Programming Interface"
---
An **API** (Application Programming Interface) is a set of protocols,
routines, and tools for building software applications...
Behavior:
- Scans posts to identify glossary definitions (by path or
templateKey) - Builds a lookup table of terms and aliases
- In non-glossary posts, finds term occurrences and wraps them in links
- Protects content inside
<a>,<code>, and<pre>tags from linking - Respects
max_links_per_termto avoid over-linking - Exports
glossary.jsonto the output directory
HTML output:
<!-- Before -->
<p>You can use the API to fetch data.</p>
<!-- After -->
<p>You can use the <a href="/glossary/api/" class="glossary-term" title="Application Programming Interface - a set of protocols for building software">API</a> to fetch data.</p>
Exported glossary.json:
{
"terms": [
{
"term": "API",
"slug": "api",
"description": "Application Programming Interface - a set of protocols for building software",
"aliases": ["APIs", "Application Programming Interface"],
"href": "/glossary/api/"
}
]
}
CSS styling:
.glossary-term {
text-decoration: underline dotted;
text-underline-offset: 2px;
cursor: help;
}
.glossary-term:hover {
text-decoration: underline solid;
}
csv_fence ¶ #
Name: csv_fence
Stage: Render (post_render)
Purpose: Converts CSV code blocks into HTML tables for easy data display.
Status: Enabled by default. Set enabled = false to disable.
Configuration (TOML):
[markata-go.csv_fence]
enabled = true # Enabled by default; set to false to disable
table_class = "csv-table" # CSS class for generated tables (default)
has_header = true # Treat first row as header (default: true)
delimiter = "," # CSV field delimiter (default: ",")
Markdown usage:
```csv
Name,Age,City
Alice,30,New York
Bob,25,Los Angeles
Charlie,35,Chicago
```
Per-block options: You can override global settings per code block:
```csv delimiter=";" has_header="false" table_class="data-table"
Alice;30;New York
Bob;25;Los Angeles
```
Behavior:
- Finds code blocks with
language-csvclass inArticleHTML - Parses the CSV content with the configured delimiter
- Generates an HTML table with
<thead>(ifhas_header) and<tbody> - Properly escapes all cell content for HTML safety
HTML output:
<table class="csv-table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>30</td>
<td>New York</td>
</tr>
<tr>
<td>Bob</td>
<td>25</td>
<td>Los Angeles</td>
</tr>
<tr>
<td>Charlie</td>
<td>35</td>
<td>Chicago</td>
</tr>
</tbody>
</table>
CSS styling:
.csv-table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}
.csv-table th,
.csv-table td {
border: 1px solid #ddd;
padding: 0.5rem;
text-align: left;
}
.csv-table th {
background: #f5f5f5;
font-weight: bold;
}
.csv-table tr:nth-child(even) {
background: #fafafa;
}
Markdown usage:
```csv
Name,Age,City
Alice,30,New York
Bob,25,Los Angeles
Charlie,35,Chicago
```
Per-block options: You can override global settings per code block:
```csv delimiter=";" has_header="false" table_class="data-table"
Alice;30;New York
Bob;25;Los Angeles
```
Behavior:
- Finds code blocks with
language-csvclass inArticleHTML - Parses the CSV content with the configured delimiter
- Generates an HTML table with
<thead>(ifhas_header) and<tbody> - Properly escapes all cell content for HTML safety
HTML output:
<table class="csv-table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>30</td>
<td>New York</td>
</tr>
<tr>
<td>Bob</td>
<td>25</td>
<td>Los Angeles</td>
</tr>
<tr>
<td>Charlie</td>
<td>35</td>
<td>Chicago</td>
</tr>
</tbody>
</table>
CSS styling:
.csv-table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}
.csv-table th,
.csv-table td {
border: 1px solid #ddd;
padding: 0.5rem;
text-align: left;
}
.csv-table th {
background: #f5f5f5;
font-weight: bold;
}
.csv-table tr:nth-child(even) {
background: #fafafa;
}
Enabling the plugin:
plugins := append(plugins.DefaultPlugins(), plugins.NewCSVFencePlugin())
Or by name:
pluginList, _ := plugins.PluginsByNames([]string{
// ... default plugins ...
"csv_fence",
})
Cleanup Stage ¶ #
pagefind ¶ #
Name: pagefind
Stage: Cleanup
Priority: Last (runs after all files are written)
Purpose: Generates a full-text search index using Pagefind after the site is built.
Configuration (TOML):
[search]
enabled = true # Enable/disable search (default: true)
position = "navbar" # Search UI position: navbar, sidebar, footer, custom
placeholder = "Search..." # Search input placeholder text
show_images = true # Show thumbnails in results (default: true)
excerpt_length = 200 # Characters for result excerpts (default: 200)
[search.pagefind]
bundle_dir = "_pagefind" # Output directory for search index (default)
root_selector = "" # CSS selector for searchable content (optional)
exclude_selectors = [] # CSS selectors to exclude from indexing
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable search functionality |
position |
"navbar" |
Where to show search UI |
placeholder |
"Search..." |
Search input placeholder |
show_images |
true |
Show thumbnails in results |
excerpt_length |
200 |
Characters for result excerpts |
pagefind.bundle_dir |
"_pagefind" |
Output directory for index |
pagefind.root_selector |
"" |
CSS selector for searchable content |
pagefind.exclude_selectors |
[] |
Elements to exclude from indexing |
Requirements:
- Pagefind CLI must be installed (standalone binary from GitHub releases, or via
auto_install = truein config) - If not installed, the plugin logs a warning but does not fail the build
Behavior:
- Runs in Cleanup stage with
PriorityLast(after all HTML files are written) - Checks if search is enabled in configuration
- Checks if
pagefindCLI is available in PATH - Executes
pagefind --site {output_dir}with configured options - Generates search index in
{output_dir}/_pagefind/
Generated files:
output/
_pagefind/
pagefind.js # Main search library
pagefind-ui.js # UI component
pagefind-ui.css # UI styles
pagefind.*.pf_index # Index chunks
pagefind.*.pf_meta # Metadata chunks
Template integration:
The default post template includes data-pagefind-* attributes for indexing:
<article data-pagefind-body>
<h1 data-pagefind-meta="title">{{ post.title }}</h1>
<p data-pagefind-meta="excerpt">{{ post.description }}</p>
{% for tag in post.tags %}
<span data-pagefind-filter="tag" style="display:none">{{ tag }}</span>
{% endfor %}
<div class="content">{{ body | safe }}</div>
</article>
Search UI component:
The base template includes the search component:
{% if config.search.enabled %}
{% include "components/search.html" %}
{% endif %}
Disabling search:
[search]
enabled = false
Graceful degradation:
- If Pagefind is not installed, search UI is hidden
- If search is disabled, no index is generated
- Site functions normally without search functionality
Lazy loading:
Pagefind CSS and JS are loaded on user interaction (hover on search, press /, press Ctrl+K) rather than eagerly on page load. This saves ~30-50KB on initial page load for pages where search is not used.
See Search Guide for detailed usage and customization.
css_minify ¶ #
Name: css_minify
Stage: Write (PriorityLast)
Purpose: Minifies all CSS files in the output directory to reduce file sizes and improve performance scores.
Configuration (TOML):
[markata-go.css_minify]
enabled = true # Enable CSS minification (default: true)
exclude = ["variables.css"] # Files to skip (exact names or glob patterns)
preserve_comments = ["Copyright"] # Strings that mark comments to preserve
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable CSS minification |
exclude |
[] |
File patterns to skip (supports *, ?, [ glob syntax) |
preserve_comments |
[] |
Substrings identifying comments to keep in output |
Behavior:
- Runs with
PriorityLastin Write stage (after all CSS-generating plugins:palette_css,chroma_css,css_bundle) - Walks the output directory recursively for
.cssfiles - Skips files matching exclusion patterns (exact match or glob)
- Extracts comments containing
preserve_commentssubstrings - Minifies content using
tdewolff/minify/v2CSS minifier - Prepends preserved comments to minified output
- Writes minified content back to the same file
- Logs statistics: files processed, files skipped, total size reduction percentage
Example output:
[css_minify] Starting minification
[css_minify] Completed: 8 files processed, 1 skipped
[css_minify] Size reduction: 45230 -> 28940 bytes (36.0% smaller)
Related plugins:
- [[#palette_css|palette_css]] - Generates palette CSS (minified by this plugin)
- [[#chroma_css|chroma_css]] - Generates syntax highlighting CSS (minified by this plugin)
- [[#js_minify|js_minify]] - Companion JS minification plugin
js_minify ¶ #
Name: js_minify
Stage: Write (PriorityLast)
Purpose: Minifies all JavaScript files in the output directory to reduce file sizes and improve performance scores.
Configuration (TOML):
[markata-go.js_minify]
enabled = true # Enable JS minification (default: true)
exclude = ["pagefind-ui.js"] # Files to skip (exact names or glob patterns)
Options:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable JS minification |
exclude |
[] |
File patterns to skip (supports *, ?, [ glob syntax) |
Behavior:
- Runs with
PriorityLastin Write stage (after all JS-generating plugins) - Walks the output directory recursively for
.jsfiles - Automatically skips
.min.jsfiles (already minified) - Skips files matching exclusion patterns (exact match or glob)
- Minifies content using
tdewolff/minify/v2JS minifier - Writes minified content back to the same file
- Logs statistics: files processed, files skipped, total size reduction percentage
Example output:
[js_minify] Starting minification
[js_minify] Completed: 12 files processed, 2 skipped
[js_minify] Size reduction: 145337 -> 72100 bytes (50.4% smaller)
Typical reduction by file:
| File | Original | Minified | Reduction |
|---|---|---|---|
view-transitions.js |
13.3KB | ~6.5KB | ~51% |
conditional-css.js |
~3KB | ~1.5KB | ~50% |
copy-code.js |
~2KB | ~1KB | ~50% |
Related plugins:
- [[#css_minify|css_minify]] - Companion CSS minification plugin
Disabling Plugins ¶ #
To use only specific plugins, configure them explicitly:
import "github.com/example/markata-go/pkg/plugins"
// Minimal set for basic builds
minimalPlugins := plugins.MinimalPlugins()
// Or pick specific plugins
customPlugins := []lifecycle.Plugin{
plugins.NewGlobPlugin(),
plugins.NewLoadPlugin(),
plugins.NewRenderMarkdownPlugin(),
plugins.NewPublishHTMLPlugin(),
}
Or use plugin names:
pluginList, warnings := plugins.PluginsByNames([]string{
"glob",
"load",
"render_markdown",
"publish_html",
})
Creating Custom Plugins ¶ #
Implement the lifecycle.Plugin interface and optionally stage-specific interfaces:
type MyPlugin struct{}
func (p *MyPlugin) Name() string {
return "my_plugin"
}
// Implement stage interfaces as needed:
// - lifecycle.ConfigurePlugin
// - lifecycle.GlobPlugin
// - lifecycle.LoadPlugin
// - lifecycle.TransformPlugin
// - lifecycle.RenderPlugin
// - lifecycle.CollectPlugin
// - lifecycle.WritePlugin
// - lifecycle.CleanupPlugin
func (p *MyPlugin) Transform(m *lifecycle.Manager) error {
return m.ProcessPostsConcurrently(func(post *models.Post) error {
// Process each post
post.Set("my_field", "my_value")
return nil
})
}
Register your plugin:
plugins.RegisterPluginConstructor("my_plugin", func() lifecycle.Plugin {
return &MyPlugin{}
})
See Plugin Development Guide for complete instructions.
-
This is the footnote content. ↩︎