Skip to content
Documentation GitHub
Content

Wiki-Link System

Status: Implemented Depends On: Page System


The Wiki-Link System provides inter-page linking using [[Display Text|target-slug]] syntax. Links are stored as inline markup in block content and tracked in a persistent references table that enables backlink queries, ghost link detection, and rename propagation.

The system distinguishes resolved links (target page exists) from ghost links (target slug has no matching page). Ghost links render with distinct CSS styling in the editor, signaling to the user that the target page doesn’t exist yet. The reference index is rebuilt as a write-time side effect after each block save.



Framework (Tauri)
└── Tauri commands apps/desktop/src-tauri/src/commands/
check_links_resolution, get_backlinks, get_outgoing_links
Application
├── Wiki-link parser crates/application/src/page/wiki_link_parser.rs
│ parse_wiki_links(), replace_wiki_link_slug(),
│ replace_wiki_link_display_and_slug(), format_wiki_link()
├── UpdateReferencesUseCase crates/application/src/page/update_references.rs
│ Write-time side effect: parse + resolve + index
└── ReferenceRepository (trait) crates/application/src/page/services.rs
update_references, get_backlinks, get_outgoing,
get_ghost_links, update_target_slug
Domain
└── Reference (entity) crates/domain/src/reference.rs
Resolved and unresolved variants, heading anchor support
Infrastructure
├── SqliteReferenceRepository crates/infrastructure/sqlite/src/workspace/reference_repository.rs
│ references table with source/target/slug indexes
└── Import conversion crates/infrastructure/import/
Obsidian [[target|alias]] to [[alias|target-slug]] syntax mapping
Frontend
└── WikiLink TipTap extension apps/desktop/src-react/
Inline node with pageName + pageSlug attributes,
heading fragment extraction, ghost CSS styling

Dependencies flow inward: Framework -> Infrastructure -> Application -> Domain.


[[Display Text|target-slug]]
[[Display Text|target-slug#heading]]
  • Display text (before the pipe): The visible text rendered in the editor. Preserved exactly as authored.
  • Target slug (after the pipe): The page’s slug used for resolution. Case-insensitive matching during rename propagation.
  • Heading fragment (after #, optional): Deep link to a specific heading within the target page.
InputValid?Reason
[[Hero|hero]]YesStandard link
[[Hero|hero#background]]YesLink with heading anchor
[[Hero]]NoNo pipe separator (silently skipped)
[[|hero]]NoEmpty display text
[[Hero|]]NoEmpty slug
[[Hero|hero#]]YesTrailing # treated as no heading

The parser (parse_wiki_links) is tolerant of whitespace around components — [[ Hero | hero ]] parses correctly with trimmed values.

Obsidian uses [[target|alias]] syntax (slug first, alias second — opposite order). The import layer converts to Inklings format:

Obsidian: [[silver-tavern|The Silver Tavern]]
Inklings: [[The Silver Tavern|silver-tavern]]

After each block save, UpdateReferencesUseCase runs as a side effect:

  1. Load the page and all its blocks
  2. Parse wiki-links from every block’s content via parse_wiki_links()
  3. For each parsed link, attempt to resolve the target slug to a page UUID
  4. Create Reference::resolved(...) or Reference::unresolved(...) accordingly
  5. Atomically replace all references for that source page in the references table

This is a full replace, not an incremental diff — all previous references from the source page are deleted and the new set is inserted in a single transaction.

pub struct Reference {
pub source_page_id: Uuid, // Page containing the link
pub target_page_id: Option<Uuid>, // Resolved target (None for ghost links)
pub display_text: String, // "The Silver Tavern"
pub target_slug: String, // "silver-tavern"
pub heading: Option<String>, // "background" (from #heading)
pub resolved: bool, // true if target exists
}

Two constructors enforce the resolved/unresolved invariant:

  • Reference::resolved(source, target, display, slug, heading) — sets target_page_id = Some(target) and resolved = true
  • Reference::unresolved(source, display, slug, heading) — sets target_page_id = None and resolved = false

Ghost links are references whose target slug does not match any existing page. They are:

  • Stored: In the references table with target_page_id = NULL and resolved implicitly false
  • Queried: Via ReferenceRepository::get_ghost_links() which returns all unresolved references workspace-wide
  • Rendered: In the TipTap editor with a distinct “ghost” CSS class, signaling that the target page doesn’t exist yet
  • Detected: The check_links_resolution Tauri command allows the frontend to verify link status

When the target page is later created, the next reference index update for any page linking to that slug resolves the ghost link and sets target_page_id.


When a page is renamed, two updates must propagate:

replace_wiki_link_slug() and replace_wiki_link_display_and_slug() perform case-insensitive slug replacement across block content:

// Slug-only replacement (preserves display text):
replace_wiki_link_slug("See [[Hero|hero]] here.", "hero", "protagonist")
// -> "See [[Hero|protagonist]] here."
// Display + slug replacement:
replace_wiki_link_display_and_slug("See [[Hero|hero]].", "hero", "Protagonist", "protagonist")
// -> "See [[Protagonist|protagonist]]."

Heading fragments are preserved through replacement: [[Hero|hero#background]] becomes [[Protagonist|protagonist#background]].

ReferenceRepository::update_target_slug() updates the target_slug column in the references table for all references pointing to the renamed page. This keeps backlink queries functional without re-parsing all source pages.


CREATE TABLE IF NOT EXISTS "references" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_page_id TEXT NOT NULL,
target_page_id TEXT, -- NULL for ghost links
target_slug TEXT NOT NULL,
display_text TEXT NOT NULL,
heading TEXT,
FOREIGN KEY (source_page_id) REFERENCES pages(id) ON DELETE CASCADE,
FOREIGN KEY (target_page_id) REFERENCES pages(id) ON DELETE SET NULL
);
CREATE INDEX idx_references_target ON "references"(target_page_id);
CREATE INDEX idx_references_source ON "references"(source_page_id);
CREATE INDEX idx_references_slug ON "references"(target_slug);

Key schema details:

  • ON DELETE CASCADE on source: When a page is deleted, all its outgoing references are removed automatically
  • ON DELETE SET NULL on target: When a referenced page is deleted, the reference becomes a ghost link (target_page_id set to NULL)
  • Three indexes: Target (backlink queries), source (outgoing link queries), slug (rename propagation and ghost link resolution)

pub trait ReferenceRepository: Send + Sync {
/// Atomically replace all references from a source page.
fn update_references(&self, workspace_path: &Path, source_page_id: Uuid, refs: &[Reference]) -> PageResult<()>;
/// Get all references pointing TO a target page (backlinks).
fn get_backlinks(&self, workspace_path: &Path, target_page_id: Uuid) -> PageResult<Vec<Reference>>;
/// Get all references FROM a source page (outgoing links).
fn get_outgoing(&self, workspace_path: &Path, source_page_id: Uuid) -> PageResult<Vec<Reference>>;
/// Get all ghost links (references with no resolved target).
fn get_ghost_links(&self, workspace_path: &Path) -> PageResult<Vec<Reference>>;
/// Update target_slug for all references pointing to a given page.
fn update_target_slug(&self, workspace_path: &Path, page_id: Uuid, new_slug: &str) -> PageResult<()>;
}

ScenarioEntry Point
Parse links from contentparse_wiki_links() in crates/application/src/page/wiki_link_parser.rs
Update reference index after saveUpdateReferencesUseCase::execute() in crates/application/src/page/update_references.rs
Replace slug in content (rename)replace_wiki_link_slug() in crates/application/src/page/wiki_link_parser.rs
Replace display + slug (rename)replace_wiki_link_display_and_slug() in crates/application/src/page/wiki_link_parser.rs
Query backlinksReferenceRepository::get_backlinks()
Detect ghost linksReferenceRepository::get_ghost_links()
Check link resolution (frontend)check_links_resolution Tauri command
Import Obsidian linkscrates/infrastructure/import/ (syntax conversion)

  • Page System (page-system): Pages are the source and target of wiki-links

Enables inter-page linking with backlinks. See also: Layout System, Tag System.

Was this page helpful?