Wiki-Link System
Status: Implemented Depends On: Page System
Overview
Section titled “Overview”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.
Diagram
Section titled “Diagram”Architecture
Section titled “Architecture”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 stylingDependencies flow inward: Framework -> Infrastructure -> Application -> Domain.
Link Syntax
Section titled “Link Syntax”Format
Section titled “Format”[[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.
Syntax Rules
Section titled “Syntax Rules”| Input | Valid? | Reason |
|---|---|---|
[[Hero|hero]] | Yes | Standard link |
[[Hero|hero#background]] | Yes | Link with heading anchor |
[[Hero]] | No | No pipe separator (silently skipped) |
[[|hero]] | No | Empty display text |
[[Hero|]] | No | Empty slug |
[[Hero|hero#]] | Yes | Trailing # treated as no heading |
The parser (parse_wiki_links) is tolerant of whitespace around components — [[ Hero | hero ]] parses correctly with
trimmed values.
Obsidian Import Conversion
Section titled “Obsidian Import Conversion”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]]Reference Index
Section titled “Reference Index”Write-Time Indexing
Section titled “Write-Time Indexing”After each block save, UpdateReferencesUseCase runs as a side effect:
- Load the page and all its blocks
- Parse wiki-links from every block’s content via
parse_wiki_links() - For each parsed link, attempt to resolve the target slug to a page UUID
- Create
Reference::resolved(...)orReference::unresolved(...)accordingly - Atomically replace all references for that source page in the
referencestable
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.
Reference Entity
Section titled “Reference Entity”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)— setstarget_page_id = Some(target)andresolved = trueReference::unresolved(source, display, slug, heading)— setstarget_page_id = Noneandresolved = false
Ghost Links
Section titled “Ghost Links”Ghost links are references whose target slug does not match any existing page. They are:
- Stored: In the
referencestable withtarget_page_id = NULLandresolvedimplicitly 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_resolutionTauri 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.
Rename Propagation
Section titled “Rename Propagation”When a page is renamed, two updates must propagate:
1. Slug Update in Content
Section titled “1. Slug Update in Content”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]].
2. Index Update
Section titled “2. Index Update”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.
Storage Schema
Section titled “Storage Schema”references table
Section titled “references table”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 CASCADEon source: When a page is deleted, all its outgoing references are removed automaticallyON DELETE SET NULLon 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)
ReferenceRepository Trait
Section titled “ReferenceRepository Trait”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<()>;}Key Code Paths
Section titled “Key Code Paths”| Scenario | Entry Point |
|---|---|
| Parse links from content | parse_wiki_links() in crates/application/src/page/wiki_link_parser.rs |
| Update reference index after save | UpdateReferencesUseCase::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 backlinks | ReferenceRepository::get_backlinks() |
| Detect ghost links | ReferenceRepository::get_ghost_links() |
| Check link resolution (frontend) | check_links_resolution Tauri command |
| Import Obsidian links | crates/infrastructure/import/ (syntax conversion) |
Related
Section titled “Related”- 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?
Thanks for your feedback!