InkStone

Documentation Map

InkStone Documentation

Content & Syntax

Every published note starts with a YAML frontmatter block between --- delimiters. InkStone reads these fields to control publishing, routing, display, and SEO.


Minimal example

---
website: true
title: My Post
date: 2026-04-16
---

That's all you need. Everything else is optional.


Full field reference

---
# ── Publishing ──────────────────────────────────────────
website: true          # Required. Omit to keep the note private (not a web page).
type: homepage         # Optional: homepage | listing | book
                       #   homepage  — renders this note's content at the section root
                       #   listing   — auto-generates a post index at the section root
                       #   book      — uses the book template with cover/metadata header

# ── Identity ─────────────────────────────────────────────
title: My Post         # Overrides the H1 heading and filename. Wrap in quotes if it contains a colon.
slug: my-post          # URL slug. Auto-derived from title if omitted.
aliases:               # Alternate names that [[wiki-links]] will resolve to this note.
  - alternate name
  - another alias

# ── Dates ────────────────────────────────────────────────
date: 2026-04-16       # Publication date. Accepted formats: YYYY-MM-DD, DD/MM/YYYY, and more.
updated: 2026-04-20    # Last-modified date. Shown as "Updated …" in post meta and JSON-LD.

# ── Author ───────────────────────────────────────────────
author: "Jane Doe"     # String or list. Shown in post meta and JSON-LD.
# author:
#   - Jane Doe
#   - John Smith

# ── Listing & Discovery ───────────────────────────────────
summary: "..."         # Shown on listing cards. Auto-derived from the first ~200 chars if omitted.
featured: true         # Highlight in the section's featured area on the listing page.
priority: 0            # Featured posts only. Lower = higher rank. Date breaks ties.
tags:                  # Content tags. Merged with inline #hashtags from the note body.
  - python
  - philosophy

# ── Navigation ────────────────────────────────────────────
menu_order: 1          # Pin this note to the top nav. Lower number = further left.
                       # Appended after auto-generated section links.

# ── Branding (header icon & title) ───────────────────────
icon: _attachments/logo.png   # Image shown beside the site title in the header.
                               # Accepts a vault-relative path, /static/... URL, or full URL.
                               # Cascades to all child pages unless overridden lower down.
site_title: "My Brand"         # Replaces the website name displayed in the header.
                               # Also cascades to child pages.

# ── Banner image ─────────────────────────────────────────
banner: "https://example.com/image.jpg"
banner_x: 0.5          # Horizontal focal point, 0–1 (default: centre).
banner_y: 0.4          # Vertical focal point, 0–1 (default: centre).

# ── Root homepage only ───────────────────────────────────
show_search: true      # Adds a Search link to the top nav.
show_tags: true        # Adds a Tags link to the top nav.
default_theme: dark    # Initial theme for new visitors: "dark", "light", or "system" (default).
                       # "system" follows the OS prefers-color-scheme. Visitors can always
                       # override with the toggle; their choice is stored in localStorage.

# ── Multilingual ─────────────────────────────────────────────
language: en           # Root homepage only. Sets the default language for the site (e.g. "en", "ru").
lang: ru               # Per-note language code. Overrides filename suffix if both are present.
                       # Filename suffix _RU.md is equivalent to setting lang: ru in frontmatter.

# Social links — one key per platform. InkStone extracts the handle from the
# URL and renders [icon] @handle in the footer. Supported keys:
# github, mastodon, bluesky, twitter, instagram, linkedin, facebook, youtube
github: https://github.com/you
mastodon: https://mastodon.social/@you
bluesky: https://bsky.app/profile/you.bsky.social
---

Title resolution order

When no title is set in frontmatter, InkStone falls back in this order:

  1. Frontmatter title
  2. First # H1 heading in the note body
  3. Filename (without .md)

The YAML colon rule

Any string value that contains a colon (:) must be wrapped in double quotes, otherwise YAML silently parses it as a nested mapping and the field breaks.

title: "From Vault to Web: How This Works"   # correct
title: From Vault to Web: How This Works      # broken — YAML sees a nested dict

InkStone logs a WARNING to stderr and falls back to H1/filename when it detects a dict-valued title.

To embed literal " characters in a title, wrap the whole value in single quotes:

title: '"Hello World" Considered Harmful'   # → "Hello World" Considered Harmful

Accepted date formats

InkStone parses date and updated in any of these formats:

Format Example
YYYY-MM-DD 2026-04-16
YYYY-MM-DD HH:MM 2026-04-16 09:30
YYYY-MM-DD HH:MM:SS 2026-04-16 09:30:00
YYYY/MM/DD 2026/04/16
DD-MM-YYYY 16-04-2026
DD/MM/YYYY 16/04/2026

Slug and URL generation

  • slug is auto-generated from title if omitted: lowercased, spaces → hyphens, non-alphanumerics stripped.
  • The URL is /<section>/<slug> for posts in subfolders, or /<slug> for vault-root notes.
  • aliases register additional wiki-link names that all resolve to the same URL.

See also

  • Post Types — how type: homepage, type: listing, and type: book work
  • Publishing and Privacy — the website: true flag and private notes
  • Navigationmenu_order and nav pinning
  • SEO and Metadata — banner images, author, OpenGraph
  • Branding — favicon override, site icon, and header title cascade
  • Comments — Giscus comment system setup
  • Social Links — social footer icons, supported platforms, handle extraction
  • Multilingual — filename suffix routing, language toggle, UI string translations

InkStone resolves [[wiki-links]] server-side during the two-pass vault load. All standard Obsidian link forms are supported.


[[Note Title]]

Renders as a link to the note titled "Note Title". Display text defaults to the note title.


Alias / custom display text

[[Note Title|Display Text]]

The part after | becomes the link text. The target is still resolved by title.

Spaces around the | are allowed — both forms are equivalent:

[[Note Title|Display Text]]
[[Note Title | Display Text]]

Heading anchor

[[Note Title#Section Heading]]

Links to a specific heading within the target note. The anchor is slugified (lowercased, spaces → hyphens).

Display text defaults to Note Title › Section Heading. Override with |:

[[Note Title#Section Heading|Read this section]]

Block reference

[[Note Title^block-id]]

Links to a specific block (paragraph) tagged with ^block-id in the target note. See Obsidian Syntax for how to create block IDs.

[[Note Title^block-id|Custom label]]

Resolution order

InkStone builds a URL index during Pass 1 of the vault load. Each note is indexed under three keys:

  1. slugify(title) — matches [[Note Title]]
  2. slugify(filename without .md) — matches by filename even when title differs
  3. frontmatter slug — matches the slug directly
  4. aliases — each alias is indexed as a separate key

This means [[Filename|Display]], [[Title]], and [[slug]] all resolve correctly even when the three differ.

If a target is not found in the index, InkStone falls back to /<slugified-title> — the link is still generated, it just may 404.


Wiki-links resolve correctly across sections. A link from blog/My Post.md to gallery/Photo.md generates /gallery/photo — no need to include the section prefix.


Links to notes registered as type: homepage or type: listing resolve to the section URL, not the file's computed slug URL. For example, [[Blog]] resolves to /blog, not /blog/blog.


Aliases

Add aliases: to a note's frontmatter to register additional link names:

aliases:
  - my alternate name
  - another alias

Any of these names can then be used in [[wiki-links]] from other notes.


See also

Callouts are styled highlight boxes rendered from Obsidian's > [!type] blockquote syntax. Everything is processed server-side — no client-side plugins needed.


Basic callout

> [!note] Title text
> Body content here.
> More body content.
Title text

Body content here. More body content.


Collapsible callout

Add - after the closing ] to make the callout collapsed by default (rendered as a closed <details> element):

> [!warning]- Click to expand
> Hidden content here.
Click to expand

Hidden content here.

Add + to pin it open (rendered as an open <details> element — user can still collapse):

> [!tip]+ Always visible
> This starts open but can be collapsed.
Always visible

This starts open but can be collapsed.


Callout types

InkStone supports the full set of Obsidian callout types. Each gets a distinct icon and accent colour:

note

General annotations

tip

Helpful hints

info

Informational content

warning

Cautions and caveats

danger / error

Critical warnings

success / check

Positive outcomes

question / faq

Questions and FAQs

quote / cite

Quoted text

important

High-priority notes

abstract / summary

Summaries and abstracts

todo

Action items

example

Code or usage examples

bug

Known issues

Any custom type not in the list above is still rendered — it just uses the default styling.


Omitting the title

Leave the title empty to render the callout type name as the title:

> [!tip]
> Body content here.

Body content here.


Multi-paragraph body

Each > line in the blockquote is part of the callout body. Standard markdown (bold, code, links) renders inside the body.

> [!example] Code example
> Here is a usage pattern:
>
> ```python
> print("hello")
> ```
>
> And an explanation.
Code example

Here is a usage pattern:

print("hello")

And an explanation.


CSS customisation

Callout styles live in frontend/static/callouts.css (default Catppuccin-inspired theme) and frontend/static/omarchy-callouts.css (Omarchy theme). Override these files to change colours, icons, or layout.

Each callout renders with the class callout callout-{type}, so per-type targeting is straightforward:

.callout-warning { border-left-color: #f5a623; }

See also

InkStone converts ![[filename]] embed syntax into lightbox images, image sliders, video players, and audio players. All media is served directly from your vault.


Where to put files

Media files go in an _attachments/ subfolder relative to the note's folder:

Note location Attachments folder
blog/My Post.md blog/_attachments/
gallery/Photo.md gallery/_attachments/
Root Note.md _attachments/

If the file isn't found in the section's _attachments/, InkStone falls back to the vault-root _attachments/, then to ATTACHMENTS_PATH from .env. See Attachments for the full resolution order.


Single image

![[photo.jpg]]

Renders as a lightbox image. Click to open full-screen.

With caption

![[photo.jpg|Caption text here]]

Renders with a <figcaption> below the image.

Sample image with a caption

With fixed width (pixels)

![[photo.jpg|600]]

A numeric-only value is treated as a pixel width, not a caption. The image is constrained to that width.


Inline illustration

Use the inline modifier to place an image in the text flow without a lightbox. The image appears as a centered figure and does not open when clicked.

![[photo.jpg|inline]]

Combine modifiers in order: inline, then optional pixel width, then optional caption text — all in one pipe argument:

Syntax Result
![[photo.jpg\|inline]] Centered figure, no lightbox
![[photo.jpg\|inline 400]] Inline, max-width 400 px
![[photo.jpg\|inline A mountain trail]] Inline with caption
![[photo.jpg\|inline 400 A mountain trail]] Inline, width + caption

The width must be a plain integer immediately after inline. Anything after the number is treated as caption text.


Place multiple embeds on separate lines to create a thumbnail gallery. Clicking any image opens the lightbox viewer:

![[photo1.jpg]]
![[photo2.jpg]]
![[photo3.jpg]]

Image slider

Place multiple embeds on a single line (space-separated) to create a swipeable slider:

![[photo1.jpg]] ![[photo2.jpg]] ![[photo3.jpg]]

Renders with left/right arrows and dot navigation.


Video

![[video.mp4]]

Supported formats: .mp4, .webm, .mov. Renders as an HTML5 <video> element with controls.


Audio

![[track.mp3]]

Supported formats: .mp3, .ogg, .wav, .flac, .m4a. Renders as an HTML5 <audio> element with controls.


Missing media

If the file cannot be found in any of the attachment locations, InkStone renders:

<em>Missing media: filename.jpg</em>

No broken image icons. Check the attachment folder structure if this appears.


Security note

InkStone validates that resolved file paths stay within the vault directory. Symlinks or paths that escape the vault are rejected and render as missing.


See also

Transclusion lets you embed the content of one note inside another. InkStone resolves ![[Note Title]] embeds server-side — the target note's body is rendered inline, wrapped in a styled container.


Full note transclusion

![[Note Title]]

Embeds the full body of the target note (frontmatter stripped, H1 stripped). The content is rendered as HTML and wrapped in a <div class="transclusion"> block with a link back to the source.


Section transclusion

![[Note Title#Heading]]

Embeds only the content under a specific heading — from that heading down to the next heading of equal or higher level.

If the heading is not found, the full note body is embedded as a fallback.


Aliases in transclusion

![[Note Title#Heading|Optional alias]]

The alias is silently ignored in transclusion (it's used in wiki-links). The transclusion title shown in the embed header is always derived from the target note and heading.


How it looks

The transcluded content renders as a distinct inset block:

  • Title bar — shows the note title (or "Title › Heading" for section transclusion), linked to the original note
  • Body — the target note's markdown rendered to HTML, including callouts, checkboxes, and highlights

How it works

Transclusion renders these sub-features inside the embedded body:

  • Callouts
  • Checkboxes
  • Highlights (==text==)
  • Standard markdown (bold, italic, tables, code)

Nested transclusion (a transcluded note that itself contains ![[...]]) is not recursively resolved — the inner embeds are left as-is.


Target note requirements

  • The target note does not need website: true to be transcludable — private notes can be transcluded.
  • The note is looked up by title, filename stem, or slug (same resolution as Wiki-Links).
  • If the target is not found, a <em class="transclusion-missing">Note not found: Title</em> placeholder is rendered instead.

See also

InkStone renders these Obsidian-specific inline syntax elements server-side. No client-side plugin or JavaScript is needed.


Checkboxes

Standard Obsidian task lists with nested indentation support.

- [ ] Unchecked item
- [x] Checked item
    - [ ] Nested unchecked
    - [x] Nested checked
  • Unchecked item
  • Checked item
    • Nested unchecked
    • Nested checked

Renders as HTML <input type="checkbox" disabled> elements in a <ul class="checkbox-list">. Checkboxes are non-interactive (read-only display).

Nesting is based on 4-space (or tab) indentation. Multiple levels are supported.


Highlights

This is ==highlighted text== in a sentence.

This is highlighted text in a sentence.

Converts to <mark>highlighted text</mark>. Works anywhere in the body — inline with other text.

Highlight syntax inside backtick code spans is left untouched:

Use `==this==` syntax in your notes.   ← the == here is NOT converted

Use ==this== syntax in your notes. ← the == here is NOT converted


Footnotes

Standard markdown footnote syntax, processed by the footnotes extension:

Here is a claim.[^1]

[^1]: This is the footnote content.

Here is a claim.1

Footnotes are collected and rendered at the bottom of the post as a numbered list with back-links.


Block IDs

Append ^block-id at the end of a paragraph to create a named anchor target:

This is an important paragraph. ^my-anchor

Renders as:

This is an important paragraph. <span id="my-anchor"></span>

This is an important paragraph.

Other notes can then link to this block:

[[Note Title^my-anchor]]
[[Note Title^my-anchor|Jump to this section]]

Obsidian Syntax › my-anchor Jump to this section

Block IDs may contain letters, numbers, hyphens, and underscores. The anchor ID is lowercased.


Standard Markdown

InkStone also runs the full standard markdown pipeline via Python-Markdown with these extensions enabled:

Extension What it adds
fenced_code ```lang ``` code blocks
tables GFM-style pipe tables
toc Auto-generated table of contents anchors
md_in_html Markdown inside raw HTML blocks
codehilite Syntax highlighting in code blocks
footnotes [^1] footnote syntax

See also


  1. This is the footnote content. 

InkStone supports two diagram/formula systems: KaTeX for mathematical notation and Mermaid for flowcharts and diagrams. Both are rendered client-side in the browser.


LaTeX / KaTeX

Math expressions are written in standard LaTeX syntax and rendered by KaTeX.

Inline math

Wrap expressions in single $ delimiters:

The formula is $E = mc^2$ inline.

Renders as: The formula is E = mc^2 inline.

Block math

Wrap expressions in double $$ delimiters:

$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$

Renders as:

\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}

How it works

InkStone protects math expressions from the markdown parser before standard rendering:

  • $$...$$<div class="math-block">...</div>
  • $...$<span class="math-inline">...</span>

The KaTeX library (loaded in base.html) then renders these elements in the browser. Expressions inside backtick code spans are left untouched.

KaTeX limitations

KaTeX supports most common LaTeX math commands. It does not support every package or macro from full LaTeX/MathJax. See the KaTeX support table for the full list of supported functions.


Mermaid diagrams

Fenced code blocks with the mermaid language tag are rendered as diagrams:

```mermaid
graph LR
    A[Start] --> B{Decision}
    B -->|Yes| C[Do this]
    B -->|No| D[Do that]
```
graph LR A[Start] --> B{Decision} B -->|Yes| C[Do this] B -->|No| D[Do that]

Supported diagram types

Mermaid supports a wide range of diagram types:

Type Syntax keyword
Flowchart graph LR / graph TD
Sequence diagram sequenceDiagram
Class diagram classDiagram
State diagram stateDiagram-v2
Entity–relationship erDiagram
Gantt chart gantt
Pie chart pie
Gitgraph gitGraph

Theme adaptation

Mermaid diagrams automatically adapt to the active dark or light theme. When the user toggles the theme, the diagrams re-render with matching colours.

How it works

Mermaid is loaded as a client-side library in base.html. Fenced mermaid blocks are passed through the markdown pipeline as literal <code class="language-mermaid"> blocks, which Mermaid's initializer then finds and renders into SVG.


See also

Publishing & Structure

The type: frontmatter field controls how InkStone registers and renders a note. There are four types.


Regular post (default)

No type: field (or any unrecognised value):

---
website: true
title: My Post
date: 2026-04-16
---
  • Served at /<section>/<slug> or /<slug> for vault-root notes.
  • Appears in section listings, search, and RSS feeds.
  • Uses the post.html template.

type: homepage

type: homepage
  • Renders this note's own markdown content at the section root URL.
  • Root note (/) — the site homepage, uses index.html.
  • Subfolder note (blog/) — the section homepage at /blog, uses index.html.
  • Does not appear in listings, search, or RSS.
  • The root homepage's title becomes the global WEBSITE_NAME shown in the browser title and nav.
  • Root-only fields: show_search, show_tags.

Each section can have at most one type: homepage. If multiple files in the same folder declare type: homepage, the last one loaded wins (undefined behaviour — avoid).


type: listing

type: listing
  • Auto-generates a paginated post index at the section root URL.
  • Lists all posts in the section (and sub-sections), sorted by date.
  • Featured posts (with featured: true) appear in a separate highlighted area at the top.
  • Does not appear in listings, search, or RSS.
  • Uses the listing.html template.
  • Intro content from the listing note's own body (if any) is shown above the post list.

Each section can have at most one type: listing.

Auto-listing fallback

If a section has posts but no explicit listing or homepage file, InkStone automatically creates a minimal listing route for that section. The title is derived from the folder name.


type: book

type: book
  • A specialised post type for long-form content with a cover image and metadata header.
  • Uses the book.html template instead of post.html.
  • The first <img> paragraph in the body is extracted and rendered as a cover image in the template header — it does not appear in the body.
  • Appears in listings, search, and RSS like a regular post.

Typical use: book reviews, long essays, or any note where a prominent cover image + metadata header makes sense.


Choosing between listing and homepage

Need Use
Auto-generated post index for a section type: listing
Custom intro page for a section with your own content type: homepage
Site root with custom content type: homepage at vault root
Book/long-form post with cover image type: book
Regular post omit type

See also

InkStone gives you fine-grained control over which notes appear on the public web. The opt-in model means everything is private by default.


Publishing a note

Add website: true to any note's frontmatter to publish it:

---
website: true
title: My Post
date: 2026-04-16
---

Without this field (or with website: false), the note is not accessible as a web page.


Private notes

A note without website: true is private:

  • It does not appear on listing pages or search results.
  • Its URL returns a "this note is private" placeholder page (not a 404).
  • It is fully accessible to Dataview queries — you can reference private notes in tables and lists.
  • It can be transcluded into published notes using ![[Note Title]].
  • Its tags are collected and usable in Dataview FROM #tag queries.

This makes private notes useful as: - Drafts (visible in Dataview listings but not published) - Metadata records (referenced by other notes) - Data sources for dynamic tables


Private page placeholder

When a visitor navigates to a private note's URL, InkStone renders a private.html template instead of a 404. The page shows the note title and a back link to the parent section. This prevents broken wiki-links when you reference a private note from a published one.


Homepage and listing files

Notes with type: homepage or type: listing are registered in SECTION_ROUTES and do not appear in ALL_POSTS. They are never listed in section listings or search results — they serve as the section root page only.


Unlisted root-level notes

Notes in the vault root (no subfolder) that do not have type: homepage or type: listing are served at /<slug> with no section prefix. They do not appear in any auto-generated listing.

The intended way to surface them: - Link to them via [[wiki-links]] from other content - Pin them to the nav via menu_order


Aliases

Published notes can register alternate link names via the aliases frontmatter field. All aliases resolve to the same URL — useful when a note is known by multiple names across your vault.

aliases:
  - quick reference
  - cheat sheet

See also

InkStone builds all site navigation automatically from vault structure and frontmatter. No config files or menus to maintain.


Top navigation bar

Every top-level vault folder that has a type: listing or type: homepage note appears as a nav link. The link text is the section title; the URL is the section root.

Example vault structure:

blog/
  Blog Index.md    ← type: listing → nav link "Blog" → /blog
gallery/
  Gallery.md       ← type: homepage → nav link "Gallery" → /gallery

The order of auto-generated section links is alphabetical by URL.

Pinned nav items

Any note with menu_order: N in its frontmatter is appended to the nav after the auto-generated section links:

menu_order: 1    # lower = further left

Multiple notes can be pinned. They are sorted by menu_order value (ascending). This is the intended mechanism for standalone pages like About or Contact.

Opt-in nav links controlled by root homepage frontmatter:

show_search: true   # adds "Search" link
show_tags: true     # adds "Tags" link

Every post page shows a breadcrumb trail: Home › Section › Post.

  • Each segment links to its URL if that URL has a section route.
  • The final segment (current post) is plain text — no link.
  • Breadcrumbs are built from the URL path, not the folder structure directly.

Previous / next post navigation

Each post page shows ← Older and Newer → links at the bottom, navigating to the adjacent post within the same section, ordered by date.

  • Posts without a date are excluded from prev/next ordering.
  • Adjacent posts are computed within the same section — cross-section navigation is not included.

Below each post, InkStone shows up to 4 related posts. The relevance score is:

score = (shared tags × 2) + (same section ? 1 : 0)

Ties are broken by date (newest first). Posts with no shared tags and no section match are not shown.

Related posts are pre-computed at vault load time — O(n²) once, O(1) per request.


Mobile navigation

On narrow viewports, the nav wraps below the site title into a compact row. No hamburger — all links remain visible and tappable. The theme toggle is pinned to the top-right corner.


See also

InkStone collects tags from two sources per note: the tags: frontmatter list and inline #hashtag mentions in the note body. Both are merged into a single tag set.


Frontmatter tags

tags:
  - python
  - philosophy
  - open-source

Tags are lowercased and normalised at load time.


Inline hashtags

Any #word in the note body that starts with a letter is collected as a tag:

This post is about #python and #obsidian workflows.

These are merged with frontmatter tags — no duplicates.

Pattern: #([A-Za-z][A-Za-z0-9_-]*) — must start with a letter, may contain letters, numbers, hyphens, and underscores.


Tag display

On post pages, tags render as clickable badge links. Clicking a tag navigates to its archive page.


Tag archive pages

Each tag has its own archive page at /tag/<name>:

/tag/python       — all posts tagged "python"
/tag/philosophy   — all posts tagged "philosophy"

Archive pages show posts sorted by date (newest first) with prev/next navigation within the tag.


Tag index

Enable the /tags page by adding show_tags: true to the root homepage's frontmatter:

show_tags: true

This adds a Tags link to the top navigation. The tags index at /tags lists every tag with its post count, sorted alphabetically.


Search filter

The search page at /search includes a tag filter dropdown. Users can narrow results to a specific tag while also entering a text query.


Dataview filtering

Use tags in FROM and WHERE clauses to query notes by tag:

```dataview
TABLE date, summary
FROM #inkstone
SORT date DESC
LIMIT 3
```
File date summary
Obsidian Bases 2026-04-30 Publish .base database views as filtered, sorted HTML tables — filename markers, filter syntax, and available fields.
Media Embeds 2026-04-24 Images, video, and audio embeds — lightbox, sliders, captions, inline illustrations, and width control.
Themes 2026-04-24 Dark, light, and system mode toggle, CSS theme architecture, and how to select or create a theme.
```dataview
LIST
FROM #documentation
SORT date DESC
LIMIT 5
```

Tags on private notes

Tags are collected from all vault notes during Pass 1, including notes without website: true. This means private notes contribute to Dataview tag queries even though they don't appear on the public site.


See also

Listing pages automatically paginate regular posts when there are more than 20. Featured posts are not paginated — they always appear at the top.


How it works

  • Page size: 20 regular posts per page.
  • Featured posts: shown in full above the pagination — never paginated regardless of count.
  • Sort order: newest first (by date). Posts without a date appear after dated posts.
  • URL parameter: ?page=N — page numbers start at 1.

Example URLs:

/blog          → page 1 (default)
/blog?page=2   → page 2
/blog?page=3   → page 3

Out-of-range pages

If ?page=N exceeds the total number of pages, InkStone clamps the value to the last valid page. No 404 for out-of-range page numbers.


Listing template

The listing.html template renders:

  1. Featured posts — horizontally highlighted cards (all, no pagination)
  2. Regular posts — cards, 20 per page
  3. Pagination controls — prev/next page links with current page indicator

Section scope

Pagination covers posts in the section and all sub-sections. A listing at /blog paginates both blog/*.md and blog/series/*.md posts together.


See also

  • Post Typestype: listing creates the listing page
  • Navigation — how section listing pages appear in the nav

Media files referenced with ![[filename]] are served directly from your vault via the /attachments/<path> route. InkStone searches three locations in order when resolving a filename.


Resolution order

When InkStone sees ![[photo.jpg]] in a note at blog/My Post.md, it checks:

  1. <section>/_attachments/photo.jpg — e.g. blog/_attachments/photo.jpg
  2. _attachments/photo.jpg — vault root attachments folder
  3. <ATTACHMENTS_PATH>/photo.jpg — custom path from .env

The first match wins. If no match is found, the embed renders as <em>Missing media: photo.jpg</em>.


Place media files in an _attachments/ subfolder relative to the note's folder:

vault/
  blog/
    My Post.md
    _attachments/
      photo.jpg
      video.mp4
  gallery/
    Photo Gallery.md
    _attachments/
      image1.jpg
      image2.jpg
  _attachments/
    shared-logo.png   ← available to all notes as fallback

Vault root fallback

Files in the vault root _attachments/ are available to all notes regardless of section. Useful for shared assets like logos, icons, or common diagrams.


Custom ATTACHMENTS_PATH

Set ATTACHMENTS_PATH in .env to point to a directory outside the vault:

ATTACHMENTS_PATH=/home/user/shared-media

This is the third and last fallback. Useful when media files live in a separate folder from the vault (e.g. a shared photos directory).


Security

InkStone validates that all resolved file paths stay within the vault directory using os.path.realpath(). Symlinks or path-traversal sequences that escape the vault are rejected and render as missing media. The ATTACHMENTS_PATH fallback is checked separately and not subject to this constraint.


Serving attachments

Attachments are served by Flask's send_from_directory at /attachments/<path>. The path is relative to VAULT_PATH. Direct URL access: /attachments/blog/_attachments/photo.jpg.


See also

InkStone has two independent translation mechanisms: content translation for publishing notes in multiple languages, and UI string translation for localising all fixed labels in templates.


Content Translation — _LANG filename suffix

Add a two-letter ISO language code as a suffix to any note filename to mark it as a translation:

blog/My Post.md        → /blog/my-post          (default language)
blog/My Post_RU.md     → /blog/my-post/ru
blog/My Post_FR.md     → /blog/my-post/fr
blog/My Post_DE.md     → /blog/my-post/de

The suffix is case-insensitive. You can also set the language explicitly in frontmatter instead of (or in addition to) the suffix:

---
website: true
lang: ru
title: Мой Пост
---

Setting the default language

The site's default language is read from language: in the root homepage's frontmatter:

---
website: true
type: homepage
language: en
---

If omitted, en is used. Notes without a language suffix are treated as the default language.

What the engine does automatically

  • A language toggle appears in the site header when the current page has translations available.
  • Navigating to a URL whose language variant doesn't exist auto-redirects to the default language and shows a "not yet translated" banner.
  • hreflang meta tags are injected on all pages for SEO.
  • Section homepages and listings can also be translated with the same suffix convention.

UI String Translations — type: translations note

Every fixed string baked into the templates can be translated without editing any HTML. Create a vault note with two parts.

Frontmatter — just the type and language code:

---
type: translations
lang: ru
---

Note body — a fenced yaml block with the string pairs:

```yaml
Search: Поиск
Tags: Теги
"All tags": Все теги
"No results": Нет результатов
for: для
result: Результат
results: Результаты
tagged: с тегом
"min read": мин чтения
Featured: Избранное
"All Posts": Все Посты
"No posts yet.": Пока нет записей.
"No tags yet.": Пока нет тегов.
"built with": создано с помощью
Home: Главная
Contents: Содержание
"See also": Смотрите также
Updated: Обновлено
by: автор
date_format: "{day} {month} {year}"
January: января
February: февраля
March: марта
April: апреля
May: мая
June: июня
July: июля
August: августа
September: сентября
October: октября
November: ноября
December: декабря
"Not yet translated": Ещё не переведено
"Translation unavailable": Перевод недоступен
"This page is not yet available in": Эта страница ещё не доступна на языке
"Read it in": Читать на
```

The strings live in the note body rather than nested in frontmatter — much easier to edit in Obsidian's editor.

No `website: true` needed

Translation notes are loaded automatically regardless of website: status. Place them anywhere in the vault.

Rules: - One note per language. Two notes with the same lang: log a warning; the last one loaded wins. - Only keys that exist in the block are translated. Missing keys fall back to the English default. - Strings that contain a colon must be quoted (standard YAML rule).


Date localisation

Dates are rendered using date_format and individual month name keys. The default format is {month} {day}, {year} (e.g. "April 25, 2026"). To use a different order or separator, set date_format and translate each month name:

date_format: "{day} {month} {year}"
January: января
February: февраля
# … all 12 months

The placeholders {day}, {month}, and {year} are always available. Month names are looked up by their English name (January, February, …) so you only need to include the languages you're adding.


The Search and Tags nav links automatically append ?lang=<code> when the visitor is on a non-default language page, so /search and /tags load with the correct UI language. The search form also preserves the language across submissions.


Translating section index pages

Section listing pages (type: listing) and homepage files (type: homepage) can also be translated:

blog/Blog Index.md      → /blog          (default)
blog/Blog Index_RU.md   → /blog/ru

The translated listing page will show only posts that have a _RU variant.


Example vault structure

My Vault/
  homepage.md              → /
  homepage_RU.md           → /ru
  _UI Translations_RU.md   → (loaded for UI labels, no URL)
  blog/
    Blog Index.md          → /blog
    Blog Index_RU.md       → /blog/ru
    My Post.md             → /blog/my-post
    My Post_RU.md          → /blog/my-post/ru

InkStone reads frontmatter to determine how a note is published. Two plugins handle auto-generating that frontmatter when you create a new note: QuickAdd (recommended) and Obsidian's built-in Templates plugin.

The demo vault ships with five ready-to-use templates in the templates/ folder, one for each page type.


Available templates

QuickAdd command Template file Page type
New Post templates/web page template.md Regular blog post or standalone page
New Homepage templates/homepage template.md Section root with custom content (type: homepage)
New Listing Page templates/listing page template.md Auto-generated post index (type: listing)
New Book templates/book template.md Book entry with cover/author layout (type: book)
New Translations Note templates/translations template.md UI label overrides for a language (type: translations)

QuickAdd is a community plugin that prompts you for values when creating a note, then fills the frontmatter automatically.

Setup (already done in the demo vault): 1. Install QuickAdd from Community Plugins. 2. Each template above is already registered as a command — no extra configuration needed. 3. All five commands are available via Ctrl+P.

Usage: 1. Press Ctrl+P and type the command name (e.g. New Post). 2. QuickAdd prompts for the required fields (title, summary, author — depending on the template). 3. Choose which folder to place the note in. 4. A new note opens with the date filled in and all frontmatter ready.

{{VALUE:Label}} prompts for user input. {{DATE:format}} inserts today's date. Both are QuickAdd syntax — they do nothing in a plain text editor.


Template contents

New Post

---
website: true
title: {{VALUE:Title}}
date: {{DATE:YYYY-MM-DD}}
summary: "{{VALUE:Summary}}"
tags:
  - 
---

New Homepage

---
website: true
type: homepage
title: {{VALUE:Title}}
date: {{DATE:YYYY-MM-DD}}
language: en
show_search: false
show_tags: false
---

New Listing Page

---
website: true
type: listing
title: {{VALUE:Title}}
date: {{DATE:YYYY-MM-DD}}
summary: "{{VALUE:Summary}}"
---

New Book

---
website: true
type: book
title: {{VALUE:Title}}
date: {{DATE:YYYY-MM-DD}}
author: "{{VALUE:Author}}"
summary: "{{VALUE:Summary}}"
tags:
  - 
---

New Translations Note

Frontmatter:

---
type: translations
lang: {{VALUE:Language code (e.g. ru, fr, de)}}
---

Note body (a fenced yaml block):

Search: 
Tags: 
"All tags": 
"No results": 
for: 
result: 
results: 
tagged: 
"min read": 
"Not yet translated": 
"Translation unavailable": 
"This page is not yet available in": 
"Read it in": 
Featured: 
"All Posts": 
Translations notes don't need `website: true`

They are loaded automatically regardless of that field. The strings live in the note body, not in frontmatter — much easier to edit in Obsidian.


Simpler alternative: Core Templates

Obsidian's built-in Templates plugin inserts a template file into the current note. It supports {{date}} and {{title}} (the file name) but cannot prompt for other values.

Setup: 1. Enable Templates in Settings → Core plugins. 2. Set the Template folder location to templates/. 3. Open a new note, then run Templates: Insert template from the command palette and pick the template you want.

Use this if you prefer no prompts and are happy to fill fields manually.


Optional frontmatter fields

The templates only include the fields each type always needs. Add these manually when required:

Field When to add
featured: true Highlight on the section listing page
updated: YYYY-MM-DD Show "Updated …" separately from date
slug: custom-slug Override the auto-generated URL slug
lang: ru Mark as a translation variant
banner: "url" Hero image at the top of the post
banner_x / banner_y Focal point for the banner image (0–1)
menu_order: 1 Pin to the top navigation bar
priority: 0 Sort order among featured posts

See Frontmatter Reference for the full list.


Customising templates

Edit any file in templates/ directly in Obsidian. To add an author prompt to the post template:

author: {{VALUE:Author (leave blank to skip)}}

QuickAdd's full template syntax: github.com/chhoumann/quickadd.

Discovery & Search

InkStone provides full-text search at /search. Search is server-side — no index file or JavaScript search library needed.


Add show_search: true to the root homepage's frontmatter to add a Search link to the top navigation:

---
website: true
type: homepage
show_search: true
---

Without this flag, /search still works — it just isn't linked in the nav.


Navigate to /search and enter a query. Results update on form submission.

URL parameters:

Parameter Description
q Text query string
tag Tag filter (exact match, case-insensitive)

Example: /search?q=python&tag=tutorial


What is searched

The search checks two fields per post:

  1. Title — case-insensitive substring match
  2. Content — the full rendered post text (HTML tags stripped, lowercased)

A post matches if the query appears in either field.


Tag filter

The search page shows a dropdown of all tags across published posts. Selecting a tag narrows results to posts with that tag in addition to the text query. Both filters apply simultaneously.

The tag list is pre-computed at vault load time from all published posts.


Query highlighting

Matched query terms are highlighted in the result list:

  • Title — matched terms wrapped in <mark> tags
  • Summary — matched terms wrapped in <mark> tags

Search scope

Search covers only published posts (website: true). Private notes, homepage files, and listing files are excluded.


See also

InkStone implements a server-side Dataview engine that executes TABLE and LIST queries from fenced ```dataview ``` blocks. Queries run at vault-load time — no client-side plugin needed.

Editor note

This feature mirrors the Obsidian Dataview plugin. Writing in Obsidian means you can preview queries live before publishing.


TABLE query

```dataview
TABLE date, summary
FROM #documentation
SORT date DESC
LIMIT 3
```
File date summary
Obsidian Bases 2026-04-30 Publish .base database views as filtered, sorted HTML tables — filename markers, filter syntax, and available fields.
Media Embeds 2026-04-24 Images, video, and audio embeds — lightbox, sliders, captions, inline illustrations, and width control.
Themes 2026-04-24 Dark, light, and system mode toggle, CSS theme architecture, and how to select or create a theme.

Each row in the result corresponds to a vault note. The first column is always the note title (as a link) unless WITHOUT ID is used.

Column aliases

```dataview
TABLE date as "Published", summary as "Description"
FROM #inkstone
SORT date DESC
LIMIT 3
```
File "Published" "Description"
Obsidian Bases 2026-04-30 Publish .base database views as filtered, sorted HTML tables — filename markers, filter syntax, and available fields.
Media Embeds 2026-04-24 Images, video, and audio embeds — lightbox, sliders, captions, inline illustrations, and width control.
Themes 2026-04-24 Dark, light, and system mode toggle, CSS theme architecture, and how to select or create a theme.

The as "Label" syntax renames a column header.

WITHOUT ID

```dataview
TABLE WITHOUT ID title, date
FROM #documentation
SORT date ASC
LIMIT 3
```
title date
Deployment 2026-04-15
Features 2026-04-15
Getting Started 2026-04-15

Suppresses the default "File" (title/link) first column.


LIST query

```dataview
LIST
FROM #inkstone
SORT date DESC
LIMIT 5
```

Renders as a <ul> of linked note titles.

LIST with extra field

```dataview
LIST summary
FROM #documentation
SORT date DESC
LIMIT 3
```
  • Obsidian Bases — Publish .base database views as filtered, sorted HTML tables — filename markers, filter syntax, and available fields.
  • Media Embeds — Images, video, and audio embeds — lightbox, sliders, captions, inline illustrations, and width control.
  • Themes — Dark, light, and system mode toggle, CSS theme architecture, and how to select or create a theme.

Appends — summary after each link.


FROM clause

Filter the source notes:

FROM syntax What it selects
FROM #tag Notes with that tag
FROM /folder Notes in that folder (not yet folder-based — use tag filtering)
Current implementation

FROM currently supports tag-based filtering (#tag). Folder-based filtering is not yet implemented.


WHERE clause

Filter by any frontmatter field or computed property:

```dataview
TABLE date
FROM #inkstone
WHERE contains(tags, "documentation")
SORT date DESC
LIMIT 3
```
File date
Obsidian Bases 2026-04-30
Media Embeds 2026-04-24
Themes 2026-04-24

Supported operators

Operator Syntax Example
AND & featured = true & date > "2026-01-01"
OR \| featured = true \| priority = 0
contains contains(field, "value") contains(tags, "inkstone")
not contains !contains(field, "value") !contains(tags, "draft")

SORT clause

Multiple sort keys (comma-separated, applied right-to-left):

```dataview
TABLE date
FROM #documentation
SORT date DESC
LIMIT 3
```
File date
Obsidian Bases 2026-04-30
Media Embeds 2026-04-24
Themes 2026-04-24

LIMIT clause

```dataview
LIST
FROM #inkstone
SORT date DESC
LIMIT 5
```

Caps the result at N rows after sorting. Always add LIMIT when the result set could be large.


GROUP BY

Flattened groups (sub-table per group)

```dataview
TABLE date, summary
FROM #inkstone
GROUP BY file.folder
```

inkstone

File date summary
InkStone 2026-04-15 A lightweight Python/Flask engine that turns a Markdown vault into a website — no export step, no build pipeline, no CMS.
Deployment 2026-04-15
Features 2026-04-15
Getting Started 2026-04-15
Architecture 2026-04-15

inkstone/docs

File date summary
Callouts 2026-04-16 Obsidian callout boxes — all types, collapsible variants, and pinned-open behavior.
Code Blocks 2026-04-16 Fenced code blocks with syntax highlighting, language labels, and a copy-to-clipboard button.
Media Embeds 2026-04-24 Images, video, and audio embeds — lightbox, sliders, captions, inline illustrations, and width control.
Math and Diagrams 2026-04-16 LaTeX math via KaTeX and Mermaid diagrams — both rendered in the browser.
RSS and Sitemap 2026-04-16 Site-wide and per-section RSS feeds, and the auto-generated sitemap.
Themes 2026-04-24 Dark, light, and system mode toggle, CSS theme architecture, and how to select or create a theme.
SEO and Metadata 2026-04-16 OpenGraph, Twitter Card, JSON-LD structured data, banner images, and author metadata.
Obsidian Bases 2026-04-30 Publish .base database views as filtered, sorted HTML tables — filename markers, filter syntax, and available fields.
Note Templates & Authoring Workflow 2026-04-23 How to create new notes with correct InkStone frontmatter — using QuickAdd or Obsidian's core Templates plugin.
Publishing and Privacy 2026-04-16 How website:true publishes a note, what private notes are, and the private page placeholder.
Obsidian Syntax 2026-04-16 Checkboxes, highlights, footnotes, and block IDs — Obsidian inline syntax rendered server-side.
Attachments 2026-04-16 Media file resolution order — section _attachments/, vault root, and ATTACHMENTS_PATH fallback.
Comments 2026-04-20 Add a comment section to posts using Giscus — GitHub Discussions as a backend.
Note Transclusion 2026-04-16 Embed another note's content inline with ![[Note Title]] or transclude a specific heading.
Hot Reload 2026-04-16 The server detects vault file changes and reloads automatically — no restart needed.
Frontmatter Reference 2026-04-16 Complete reference for all InkStone frontmatter fields.
Pagination 2026-04-16 How listing pages paginate regular posts — page size, URL parameter, and featured post behavior.
Dataview 2026-04-16 Server-side Dataview queries — TABLE, LIST, FROM, WHERE, SORT, GROUP BY, LIMIT, and inline expressions.
Branding 2026-04-17 Favicon override, site icon beside the title, and per-section header title — all controlled from frontmatter.
Search 2026-04-16 Full-text search across all published posts, with optional tag filter.
Navigation 2026-04-16 Auto-generated nav, menu_order pinning, breadcrumbs, prev/next post links, and related posts.
Social Links 2026-04-20 Add social profile links to the footer — icon + handle, auto-detected from URL.
Documentation 2026-04-16 Complete InkStone reference — every feature documented in its own note.
Multilingual & UI Translations 2026-04-23 Publish content in multiple languages and translate all fixed UI text — no template editing required.
Canvas 2026-04-24 Publish Obsidian Canvas files as read-only visual boards — nodes, edges, arrows, file previews, and media embeds rendered from the .canvas JSON.
Wiki-Links 2026-04-16 How to link between notes using Obsidian wiki-link syntax — all forms supported.
Tags 2026-04-16 Frontmatter tags and inline #hashtags — archive pages, tag index, and Dataview filtering.
Post Types 2026-04-16 The four note types: homepage, listing, book, and regular posts — what each does and when to use it.

Renders a heading for each group, then a sub-table of its rows.

Collapsed groups using rows.field

```dataview
TABLE rows.file.link as "Page", rows.date as "Date"
FROM #inkstone
GROUP BY file.folder
```
File "Page" "Date"
InkStone
Deployment
Features
Getting Started
Architecture
2026-04-15
2026-04-15
2026-04-15
2026-04-15
2026-04-15
Callouts
Code Blocks
Media Embeds
Math and Diagrams
RSS and Sitemap
Themes
SEO and Metadata
Obsidian Bases
Note Templates & Authoring Workflow
Publishing and Privacy
Obsidian Syntax
Attachments
Comments
Note Transclusion
Hot Reload
Frontmatter Reference
Pagination
Dataview
Branding
Search
Navigation
Social Links
Documentation
Multilingual & UI Translations
Canvas
Wiki-Links
Tags
Post Types
2026-04-16
2026-04-16
2026-04-24
2026-04-16
2026-04-16
2026-04-24
2026-04-16
2026-04-30
2026-04-23
2026-04-16
2026-04-16
2026-04-16
2026-04-20
2026-04-16
2026-04-16
2026-04-16
2026-04-16
2026-04-16
2026-04-17
2026-04-16
2026-04-16
2026-04-20
2026-04-16
2026-04-23
2026-04-24
2026-04-16
2026-04-16
2026-04-16

When column expressions start with rows., InkStone renders one row per group, collecting values from all rows in that group.


Inline queries

Use `= expr` anywhere in note body to evaluate a field against the current note's frontmatter:

Published: `= date`
Author: `= author`
Tags: `= join(tags, ", ")`

this.field is an alias for the field name:

`= this.title`

Available fields

Every note exposes these in queries:

Field Value
title Note title
date Publication date
updated Last-modified date
summary Summary text
tags List of tags
author Author string or list
section Vault section (e.g. blog)
featured Boolean
priority Number
file.name Note title
file.link HTML link to the note
file.folder Folder path relative to vault root
Any frontmatter field Accessible by its YAML key

Expression functions

Function Usage Result
join(field, "sep") join(tags, ", ") Joins a list with separator
join(list(a, b), "sep") join(list(title, date), " · ") Joins explicit values
link(target, text) link(file.link, title) Constructs an anchor tag

Private notes in queries

All vault notes — including those without website: true — are available to Dataview queries. This lets you use private notes as metadata sources or drafts while keeping them off the public site.


See also

InkStone can publish Obsidian .base files as filtered, sorted HTML tables. A Base is a YAML file that describes a database view over your vault notes — InkStone evaluates the filters and renders the result as a <table> inside a regular post page.


Publishing a base

The recommended way to publish a base is the filename marker: rename the file so it ends with __website before the .base extension.

All Posts__website.base

The page title is everything before __website — in this case "All Posts". The base is served at the same URL an .md file in the same folder would produce.

Why the filename marker?

Obsidian Bases does not support arbitrary YAML keys — a website: true field in the .base YAML may be stripped when Obsidian re-saves the file. The filename marker survives re-saves.

Featuring on listing pages

Add __featured to the filename to pin the base in the Featured section of the parent listing:

All Posts__website__featured.base

The suffixes can appear in either order.

Legacy: website: true YAML field

Adding website: true to the .base YAML still works as a fallback:

website: true
title: All Posts

Use the filename marker for new bases — the YAML field is there for backwards compatibility.


Base YAML structure

A .base file is a YAML document. InkStone reads the following top-level fields:

Field Purpose
title Page title (overrides filename-derived title)
slug URL slug (auto-derived from title if omitted)
date Publication date (YYYY-MM-DD)
summary Shown on listing cards
tags Content tags
featured Alternative to __featured in filename
author Shown in post meta
banner Hero image URL
banner_x / banner_y Focal point for the banner (percentage, 0100)
type View type — currently only table is rendered
filters Array of filter conditions (see below)
columns Column definitions — field (required), name (display label)
sort Sort field
sortOrder "asc" or "desc"
limit Maximum number of rows

Filters

Filters are evaluated against the dataview_index (one record per published .md note). Each filter is an object with a type field.

file.hasTag()

Matches notes that have the given tag.

filters:
  - type: file.hasTag
    tag: python

file.tags.contains()

Same as file.hasTag() — alternate syntax accepted by InkStone.

filters:
  - type: file.tags.contains
    value: "python"

file.inFolder()

Matches notes whose vault path starts with the given folder prefix.

filters:
  - type: file.inFolder
    folder: blog

Property comparison

Compares a frontmatter field against a value. Supported operators: =, !=, <, <=, >, >=.

filters:
  - type: property
    field: status
    operator: "="
    value: published

and / or / not

Combine or negate filters:

filters:
  - type: and
    filters:
      - type: file.hasTag
        tag: python
      - type: file.inFolder
        folder: blog
filters:
  - type: not
    filter:
      type: property
      field: draft
      operator: "="
      value: true

Example

A base that lists all published blog posts tagged python, sorted by date descending, showing title and date columns:

title: Python Posts
date: 2026-04-30
type: table
filters:
  - type: and
    filters:
      - type: file.inFolder
        folder: blog
      - type: file.hasTag
        tag: python
columns:
  - field: title
    name: Title
  - field: date
    name: Date
sort: date
sortOrder: desc
limit: 20

Save this as Python Posts__website.base in your vault to publish it at /python-posts.


See also

InkStone can publish Obsidian .canvas files as read-only visual boards. Nodes are rendered as positioned boxes, edges as SVG bezier curves with direction arrows, and edge labels as HTML overlays — all scaled to fit the page width.


Publishing a canvas

The recommended way to publish a canvas is the filename marker: rename the file so it ends with __website before the .canvas extension.

My Diagram__website.canvas

The page title is everything before __website — in this case "My Diagram". The canvas is served at the same URL that an .md file in the same folder would produce.

Why the filename marker?

Obsidian strips unrecognised top-level JSON keys when it re-saves a canvas. That means "website": true disappears the next time you edit the file in Obsidian. The filename marker survives re-saves because Obsidian never touches the filename.

Top-level JSON fields

These optional JSON fields are read from the canvas file:

Field Purpose
"title": "..." Overrides the filename-derived title
"date": "YYYY-MM-DD" Publication date
"summary": "..." Shown on listing cards
"tags": [...] Content tags
"featured": true Show in the Featured section of the parent listing
"banner": "..." Hero image URL

Legacy: "website": true JSON flag

Adding "website": true to the canvas JSON still works as a fallback, but it is fragile — Obsidian will remove it on the next re-save. Use the filename marker instead.


Node types

All four Obsidian node types are supported:

Type Rendered as
text Box with rendered inline markdown (bold, italic, inline code, line breaks)
file Card with a scrollable preview of the linked note, or inline media — see below
link Box with a clickable external URL (opens in a new tab)
group Dashed-border container rendered beneath other nodes; label appears on the top edge

Text nodes

Text content supports a subset of inline markdown:

  • **bold** and __bold__
  • *italic*
  • `inline code`
  • Line breaks (newlines become <br>)

File nodes

File nodes behave differently depending on what they point at:

Published post — if the file resolves to a published InkStone post, the node renders as a card with: - A clickable header showing the post title (links to the post URL) - A scrollable preview of the full rendered note body

Vault image, video, or audio — if the file points at a media file inside the vault (.jpg, .png, .gif, .webp, .svg, .mp4, .webm, .mov, .mp3, .ogg, .wav, .flac, .m4a), the media is embedded inline inside the card.

Unresolved file — if the file is not a published post or recognised media, the node shows the filename as plain text.

Node colours

Obsidian's six preset colours are supported:

Value Colour
"1" Red
"2" Orange
"3" Yellow
"4" Green
"5" Cyan
"6" Purple

The colour is applied as the node's border colour. Unset nodes use the default theme border.


Edges

Edges are drawn as SVG bezier curves with a direction arrow at the target end. The arrowhead colour matches the edge stroke.

The control-point length scales with the distance between connected sides so curves stay smooth at any layout.

Edge labels

If an edge has a "label" field, the label renders as a small HTML badge at the visual midpoint of the curve — not as SVG text, which would be distorted by the aspect-ratio scaling. The badge has a background so it stays readable over node boxes.

Edge colours

Edge colour follows the same six-value scheme as node colours. Edges without a "color" use the theme's muted text colour.


Sizing and layout

The canvas is rendered at the full width of the article column. The height is set automatically to preserve the original aspect ratio of all nodes combined (plus padding). Nodes are positioned with percentage coordinates so the layout stays correct at any viewport width.

Size nodes to fit their content

Canvas nodes have fixed sizes defined by you in Obsidian. If a text node is too small for its content, the overflow is hidden (with a subtle fade). Resize nodes in Obsidian until the content fits — what you see in Obsidian is what gets published.


Example

A minimal two-node canvas (save as Simple Diagram__website.canvas):

{
  "title": "Simple Diagram",
  "nodes": [
    {
      "id": "a",
      "type": "text",
      "text": "**Start**",
      "x": -200, "y": -60,
      "width": 160, "height": 80,
      "color": "5"
    },
    {
      "id": "b",
      "type": "text",
      "text": "**End**",
      "x": 80, "y": -60,
      "width": 160, "height": 80,
      "color": "4"
    }
  ],
  "edges": [
    {
      "id": "e1",
      "fromNode": "a", "fromSide": "right",
      "toNode": "b",   "toSide": "left",
      "label": "next"
    }
  ]
}

See also

  • Media Embeds — inline images and galleries in regular notes
  • Post Types — other special page types (homepage, listing, book)
  • Bases — publish .base database views as filtered HTML tables
  • Dataview — server-side query tables in fenced code blocks

SEO & Feeds

InkStone generates rich metadata for every page automatically — no plugins or third-party services required.


OpenGraph / Twitter Card

Every page gets <meta> tags for social sharing:

Meta tag Source
og:title / twitter:title Post title
og:description / twitter:description Post summary
og:image / twitter:image banner frontmatter URL
og:url Canonical URL of the page
og:type article for posts, website for homepage
twitter:card summary_large_image if banner set, else summary

These tags are injected in a {% block meta %} in base.html and overridden per page.


Add a banner image to any note:

banner: "https://example.com/photo.jpg"
banner_x: 0.5   # horizontal focal point, 0–1 (default: 0.5)
banner_y: 0.4   # vertical focal point, 0–1 (default: 0.5)

The banner is used as the OpenGraph / Twitter Card image. It also appears as a hero image at the top of the post page, with CSS object-position set from banner_x and banner_y so the focal point stays visible at any aspect ratio.


JSON-LD structured data

InkStone injects a <script type="application/ld+json"> block on each page for Google rich results.

Article schema (regular posts)

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Post Title",
  "datePublished": "2026-04-16",
  "dateModified": "2026-04-20",
  "author": [{"@type": "Person", "name": "Jane Doe"}],
  "image": "https://example.com/banner.jpg"
}

Book schema (type: book posts)

{
  "@context": "https://schema.org",
  "@type": "Book",
  "name": "Book Title",
  "author": [{"@type": "Person", "name": "Jane Doe"}]
}

WebSite schema (homepage)

{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "Site Name",
  "url": "https://example.com"
}

Author field

author: "Jane Doe"

Or multiple authors:

author:
  - Jane Doe
  - John Smith

Shown in post meta (below the title) and included in JSON-LD author array.


Updated date

updated: 2026-04-20

Shown as "Updated April 20, 2026" in post meta. Maps to dateModified in JSON-LD.


Reading time

Estimated reading time is computed at load time from word count (200 words per minute) and shown on post pages and listing cards.


Canonical URL

Each page's canonical URL is injected as <link rel="canonical"> using request.base_url (the URL without query parameters). This prevents duplicate-content issues with paginated listing pages.


Sitemap

All published URLs are listed in /sitemap.xml. See RSS and Sitemap for details.


See also

InkStone auto-generates RSS feeds and a sitemap for every deployment — no configuration needed.


Site-wide RSS feed

URL: /feed.xml

  • Contains the 20 most recent published posts across all sections, sorted by date (newest first).
  • Each item includes: title, link, GUID, pubDate, and description (the post summary).
  • The feed title and link use WEBSITE_NAME (from the root homepage title).

Per-section RSS feed

URL: /<section>/feed.xml

Example: /blog/feed.xml, /gallery/feed.xml

  • Contains the 20 most recent posts in that section (including sub-sections), sorted by date.
  • The section must have a registered route (a type: listing or type: homepage note, or an auto-generated listing).
  • Returns 404 if the section doesn't exist.

Feed format

Both feeds are valid RSS 2.0 XML:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>My Site</title>
    <link>https://example.com</link>
    <description>My Site</description>
    <lastBuildDate>...</lastBuildDate>
    <item>
      <title>Post Title</title>
      <link>https://example.com/blog/post-slug</link>
      <guid>https://example.com/blog/post-slug</guid>
      <pubDate>Wed, 16 Apr 2026 00:00:00 +0000</pubDate>
      <description>Post summary text...</description>
    </item>
    ...
  </channel>
</rss>

Sitemap

URL: /sitemap.xml

Auto-generated sitemap that lists every public URL on the site:

  • The root /
  • All section root URLs (from SECTION_ROUTES)
  • All regular post URLs (from ALL_POSTS)

Format: XML Sitemap Protocol 0.9.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc></url>
  <url><loc>https://example.com/blog</loc></url>
  <url><loc>https://example.com/blog/my-post</loc></url>
  ...
</urlset>

Private notes, tag archive pages, and search/tags index pages are not included.


Linking to feeds

Add feed autodiscovery to your site by referencing the feed URL in templates, or simply share the URL with subscribers. Most RSS readers also auto-detect /feed.xml.


See also

Appearance & Dev

InkStone ships with its own logo and favicon. All three branding elements — the favicon, the icon beside the site title, and the displayed title itself — can be overridden from your vault with no code changes.


Favicon

InkStone serves its built-in logo at /favicon.ico, /favicon.png, and /favicon.svg by default.

To use your own favicon, place a file named favicon.ico, favicon.png, or favicon.svg in the root of your vault. InkStone checks for it on every request and serves it in place of the default. The first match wins in this order: .ico.png.svg.

your-vault/
  favicon.ico   ← drop it here to override
  homepage.md
  blog/
  ...

No config, no restart needed — the hot-reload file watcher picks it up automatically.


Site icon (beside the title)

Add an icon: field to any note's frontmatter to show an image beside the site title in the header.

---
website: true
type: homepage
title: My Site
icon: _attachments/my-logo.png
---

Path formats

Value How it's served
_attachments/logo.png Vault-relative — served via /attachments/_attachments/logo.png
/static/logo.svg Engine static asset — served directly
https://example.com/logo.svg External URL — used as-is

Cascade inheritance

The icon cascades down the URL hierarchy. Set it once on a section homepage and every page in that section inherits it automatically.

Example: setting icon on the /inkstone homepage means all of /inkstone/getting-started, /inkstone/docs/dataview, etc. show the same icon — unless one of those pages sets its own icon: to override it.

The resolution order for any page at /a/b/c is:

  1. /a/b/c (the page itself)
  2. /a/b
  3. /a
  4. / (root homepage)

The first ancestor that has icon: set wins. If none do, no icon is shown.


Custom header title

site_title: replaces the website name displayed in the header <h1> for a page and all its children.

---
website: true
type: homepage
title: InkStone Docs
site_title: "InkStone"
---

This is useful when a section of your site has a distinct brand from the root. The cascade rules are identical to icon: — set it on a section homepage and every child page inherits it.

icon: and site_title: are independent — you can set either or both. A child page can override one without affecting the other.


Full example

Root homepage — sets the default icon and title for the whole site:

---
website: true
type: homepage
title: Anton Bakulin
icon: _attachments/avatar.jpg
---

InkStone section homepage — overrides both for everything under /inkstone:

---
website: true
type: homepage
title: InkStone
icon: /static/logo.svg
site_title: "InkStone"
---

A single post — overrides the icon for that page only:

---
website: true
title: My Photography
icon: _attachments/camera-icon.svg
---

See also

InkStone ships with two CSS themes and a dark/light mode toggle. All styling is in plain CSS — no build step.


Theme toggle

A three-state toggle in the header cycles through System → Light → Dark → System:

Icon Mode Behaviour
System Follows prefers-color-scheme; updates live if the OS theme changes
Light Always light, regardless of OS setting
Dark Always dark, regardless of OS setting

The preference is stored in localStorage. When no value is saved (or after selecting System), the site follows the OS prefers-color-scheme media query in real time.

The toggle works by setting a data-theme="light" or data-theme="dark" attribute on the <html> element. CSS variables handle colour differences so a single attribute covers the entire site.


CSS file structure

File Description
frontend/static/base.css All layout, typography, components. Uses CSS custom properties throughout. Obsidian palette is the default.
frontend/static/callouts-base.css Callout box structure and icons — theme-agnostic.
frontend/static/theme-obsidian.css Callout type colours for the Obsidian theme (dark + light).
frontend/static/theme-omarchy.css Catppuccin Mocha/Latte variable overrides + callout type colours.
frontend/static/code.css Code block styles: Tokyo Night Dark syntax highlighting, language labels, copy button.

base.css + callouts-base.css are always loaded. Only one theme-*.css file is loaded at a time, selected from the frontmatter.


Default theme for new visitors

Set default_theme in your root homepage's frontmatter to control what new visitors see before they've touched the toggle:

---
website: true
type: homepage
title: My Site
default_theme: dark   # "dark" | "light" | "system" (default)
---
Value Behaviour
dark Forces dark mode for new visitors
light Forces light mode for new visitors
system Follows the visitor's OS prefers-color-scheme (default if omitted)

Visitors can always override with the toggle; their choice is saved in localStorage and takes precedence on return visits.


Selecting a theme

Site-wide theme

Add theme to your root homepage's frontmatter:

---
website: true
type: homepage
title: My Site
theme: omarchy
---

This applies the selected theme to every page on the site. If theme is not set, the site defaults to obsidian.

Per-page theme override

Any individual page can override the site theme with its own theme frontmatter field:

---
website: true
title: My Special Page
theme: omarchy
---

That page renders with the overridden theme; all other pages continue using the site-wide default.

Priority: page frontmatter → homepage frontmatter → obsidian (built-in fallback).


Creating a new theme

The CSS token architecture means a new theme only needs to declare the variables it differs from the defaults. A theme that changes nothing but the background:

/* theme-custom.css */
:root {
    --bg: #2d1b33;
    --bg-raised: #251528;
    --banner-gradient: transparent, rgba(45,27,51,0.5), #2d1b33;
}

Place the file in frontend/static/ as theme-custom.css, then reference it in your homepage frontmatter:

theme: custom

Everything else (layout, typography, callout structure, navigation) inherits from base.css. Only the variables you declare are overridden.


CSS custom property reference

Key variables defined in base.css that themes can override:

Variable Purpose
--bg Page background
--bg-raised Elevated surfaces (code blocks, mermaid, transclusions)
--bg-card Card / form surfaces
--bg-stripe Table alternating row tint
--bg-hover Row / badge hover tint
--text Body text
--text-muted Secondary text
--text-dim Tertiary text (breadcrumbs, pagination info)
--accent Links, active borders, highlights
--site-title Site title colour in header
--border Primary border colour
--border-subtle Soft inner borders (post entries, search results)
--h1-color--h6-color Per-heading colour tokens
--callout-base-bg Default callout background
--callout-base-border Default callout border-left colour
--tag-color Tag badge text colour
--banner-meta-color Subtitle / meta text on banner images
--inline-code-bg / --inline-code-text Inline code chip
--mark-bg / --mark-fg ==highlight== background and text

A @media print stylesheet is included in base.html. When printing, it:

  • Hides the nav, header controls, and sidebar
  • Resets colours to black on white
  • Appends the full URL after every link (a[href]::after { content: " (" attr(href) ")"; })

Mobile styles

Responsive layout adjusts at 600 px viewport width:

  • Nav links wrap below the site title (no hamburger — always visible)
  • Breadcrumbs stay on a single horizontal line
  • Listing cards stack vertically
  • Font sizes scale down slightly

See also

InkStone renders fenced code blocks with syntax highlighting, a language label badge, and a copy-to-clipboard button — all without client-side plugins.


Basic usage

```python
def hello(name: str) -> str:
    return f"Hello, {name}!"
```

Renders as:

def hello(name: str) -> str:
    return f"Hello, {name}!"

Features: - Syntax highlighting — Tokyo Night Dark colour scheme via Python-Markdown's codehilite extension - Language label — shown in the top-right corner of the block - Copy button — appears on hover (or always on mobile); copies the code content to the clipboard


Supported languages

Any language supported by Pygments works. Common examples:

Language Fence tag
Python ```python
JavaScript ```javascript or ```js
TypeScript ```typescript or ```ts
Bash / Shell ```bash or ```sh
SQL ```sql
YAML ```yaml
JSON ```json
HTML ```html
CSS ```css
Rust ```rust
Go ```go
Markdown ```markdown

Unsupported or omitted language tags render as plain text in a <code> block, without a label.


Inline code

Backtick spans render as <code> elements:

Use the `print()` function.

Inline code is not syntax-highlighted — it's styled with the code font and a subtle background.


Styling

Code block styles live in frontend/static/code.css. To change the colour scheme, replace the codehilite-generated CSS classes or swap out the code.css file entirely.

The language label is a <span class="code-lang"> element positioned in the top-right corner via CSS.


Mermaid and Dataview blocks

Fenced blocks with mermaid or dataview language tags are intercepted before reaching the standard code renderer:

  • mermaid blocks → rendered as SVG diagrams (see Math and Diagrams)
  • dataview blocks → executed as server-side queries (see Dataview)

See also

InkStone watches your vault for file changes and reloads automatically on the next request. There is no file watcher process — the check happens in the request path.


How it works

On every incoming request, maybe_reload() runs:

  1. Checks the newest file modification time across all vault files.
  2. Compares it against the last known scan time.
  3. If anything changed, re-runs the full load_posts() pipeline and swaps in the new data.

A 2-second debounce prevents multiple simultaneous reloads — if a request arrives within 2 seconds of the last check, the check is skipped.


What triggers a reload

Any change to any file in the vault directory:

  • Editing a .md note
  • Creating or deleting a note
  • Adding or removing media files in _attachments/
  • Modifying frontmatter

Thread safety

Reloads are protected by a threading.Lock. If two requests arrive simultaneously and both detect a change, only one performs the reload — the other skips it and serves the previous (stale-by-milliseconds) data.


Development workflow

  1. Start the server: python3 app.py
  2. Edit notes in Obsidian (or any editor)
  3. Save and refresh the browser — the change is live

No --watch flags. No separate process. No restart.


Production note

Hot reload also works in production. When you push a vault update and Coolify rebuilds the Docker image, the new vault is loaded at startup. If you're running a long-lived process (gunicorn) with the vault mounted as a volume, changes are picked up automatically on the next request.


See also

InkStone supports opt-in comments via Giscus — a lightweight comment system backed by GitHub Discussions. When enabled, a comment section appears at the bottom of every post and book page.


How it works

Giscus stores comments as GitHub Discussions in a repository you choose. Visitors log in with their GitHub account to comment. No database, no ads, no tracking beyond what GitHub itself does.


Setup

1. Prepare your GitHub repo

The repo that holds discussions must be:

  • Public
  • Have the Discussions feature enabled (Settings → Features → Discussions ✓)

You can use your site's source repo or a dedicated repo for discussions.

2. Get your IDs from giscus.app

Go to giscus.app, enter your repo, choose a mapping (Pathname is recommended), and select a discussion category. The page will give you:

  • Repo — e.g. you/your-site
  • Repo ID — a base64 string starting with R_
  • Category ID — a base64 string starting with DIC_

3. Set environment variables

Add the three values to your .env (local) or deployment environment:

GISCUS_REPO=you/your-site
GISCUS_REPO_ID=R_kgDO...
GISCUS_CATEGORY_ID=DIC_kwDO...

When all three are set, InkStone automatically injects the Giscus embed at the bottom of post.html and book.html. If any variable is missing, comments are not shown.


Theme sync

Giscus automatically follows InkStone's dark/light mode toggle. When you switch themes, the comment section updates instantly — no page reload required.


Disabling comments per-post

There is no per-post opt-out at the moment. Comments are shown on all posts and books when the env vars are set. If you need to suppress comments on specific pages, leave the env vars unset (site-wide off) or open a feature request.


See also

  • Deployment — full environment variable reference
  • Post Types — which templates show the comment section (post.html, book.html)

InkStone can display your social profiles in the footer as icon + handle pairs. The network is detected automatically from the frontmatter key name; the handle is extracted from the URL.


Setup

Add one key per platform to your root homepage frontmatter:

---
website: true
type: homepage
title: My Site

github: https://github.com/yourname
mastodon: https://mastodon.social/@yourname
bluesky: https://bsky.app/profile/yourname.bsky.social
---

That's all. InkStone shows the platform icon, extracts @yourname from the URL, and renders it in the footer. Hovering a link shows a tooltip with the full network name and handle.


Supported platforms

Key Network Handle from URL rel="me"
github GitHub @username
mastodon Mastodon @username
bluesky Bluesky @username (.bsky.social stripped)
twitter X / Twitter @username
instagram Instagram @username
linkedin LinkedIn username
facebook Facebook username
youtube YouTube @handle

rel="me" is set on GitHub, Mastodon, and Bluesky links — the platforms used for identity verification (e.g. Mastodon profile verification).


How handles are extracted

InkStone takes the last path segment of the URL and prepends @ where appropriate:

URL Displayed as
https://github.com/airenare @airenare
https://mastodon.social/@airenare @airenare
https://bsky.app/profile/airenare.bsky.social @airenare
https://x.com/airenare @airenare
https://linkedin.com/in/airenare airenare

Ordering

Links appear in the footer in a fixed order: GitHub → Mastodon → Bluesky → X → Instagram → LinkedIn → Facebook → YouTube. The order matches the registry — any platform you haven't set simply doesn't appear.


See also