diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
new file mode 100644
index 000000000..96e4850fc
--- /dev/null
+++ b/.github/workflows/deploy-docs.yml
@@ -0,0 +1,181 @@
+# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
+# This workflow builds and deploys your MkDocs site when changes are pushed to main
+name: Deploy MkDocs Documentation
+
+on:
+ # Trigger on push to main branch
+ push:
+ branches:
+ - main
+ - master # Also support master branch
+ # Only run when docs files change
+ paths:
+ - 'docs/**'
+ - 'mkdocs.yml'
+ - 'requirements-docs.txt'
+ - '.github/workflows/deploy-docs.yml'
+ - 'scripts/fix-mkdocs-structure.ts'
+
+ # Allow manual triggering from Actions tab
+ workflow_dispatch:
+
+ # Run on pull requests for preview deployments
+ pull_request:
+ branches:
+ - main
+ - master
+ paths:
+ - 'docs/**'
+ - 'mkdocs.yml'
+ - 'requirements-docs.txt'
+ - '.github/workflows/deploy-docs.yml'
+ - 'scripts/fix-mkdocs-structure.ts'
+
+jobs:
+ build-and-deploy:
+ name: Build and Deploy MkDocs
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ # Required permissions for deployment
+ permissions:
+ contents: read
+ deployments: write
+ pull-requests: write # For PR preview comments
+ id-token: write # For OIDC authentication (if needed)
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.13'
+ cache: 'pip'
+ cache-dependency-path: 'requirements-docs.txt'
+
+ - name: Install MkDocs and Dependencies
+ run: |
+ pip install --upgrade pip
+ pip install -r requirements-docs.txt
+ env:
+ PIP_DISABLE_PIP_VERSION_CHECK: 1
+
+ # Setup pnpm before fixing docs structure
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+
+ # Setup Node.js with pnpm
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'pnpm'
+
+ # Install Node.js dependencies for the TypeScript script
+ - name: Install Dependencies
+ run: |
+ pnpm install --frozen-lockfile
+
+ - name: Fix Documentation Structure
+ run: |
+ # Fix duplicate navigation entries by moving overview pages to index.md
+ pnpm run chore:fix-mkdocs-structure
+
+ - name: Build MkDocs Site
+ run: |
+ # Build with strict mode but allow expected warnings
+ mkdocs build --verbose || {
+ EXIT_CODE=$?
+ # Check if the only issue is expected warnings
+ if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
+ [ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
+ echo "✅ Build succeeded with expected warnings"
+ mkdocs build --verbose
+ else
+ echo "❌ Build failed with unexpected errors"
+ exit $EXIT_CODE
+ fi
+ }
+
+ - name: Validate Built Site
+ run: |
+ # Basic validation that important files exist
+ test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
+ test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
+ test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
+ echo "✅ Site validation passed"
+
+ # Install wrangler globally to avoid workspace issues
+ - name: Install Wrangler
+ run: |
+ npm install -g wrangler
+
+ # Deploy using Wrangler (use pre-installed wrangler)
+ - name: Deploy to Cloudflare Pages
+ id: deploy
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ command: pages deploy site --project-name=trilium-docs --branch=${{ github.ref_name }}
+ wranglerVersion: '' # Use pre-installed version
+
+ # Deploy preview for PRs
+ - name: Deploy Preview to Cloudflare Pages
+ id: preview-deployment
+ if: github.event_name == 'pull_request'
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ command: pages deploy site --project-name=trilium-docs --branch=pr-${{ github.event.pull_request.number }}
+ wranglerVersion: '' # Use pre-installed version
+
+ # Post deployment URL as PR comment
+ - name: Comment PR with Preview URL
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const prNumber = context.issue.number;
+ // Construct preview URL based on Cloudflare Pages pattern
+ const previewUrl = `https://pr-${prNumber}.trilium-docs.pages.dev`;
+ const mainUrl = 'https://docs.triliumnotes.org';
+
+ // Check if we already commented
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber
+ });
+
+ const botComment = comments.data.find(comment =>
+ comment.user.type === 'Bot' &&
+ comment.body.includes('Documentation preview is ready')
+ );
+
+ const commentBody = `📚 Documentation preview is ready!\n\n🔗 Preview URL: ${previewUrl}\n📖 Production URL: ${mainUrl}\n\n✅ All checks passed\n\n_This preview will be updated automatically with new commits._`;
+
+ if (botComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: commentBody
+ });
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: commentBody
+ });
+ }
diff --git a/.gitignore b/.gitignore
index 66e9781f9..b2c4e3c46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,7 @@ upload
*.tsbuildinfo
/result
-.svelte-kit
\ No newline at end of file
+.svelte-kit
+
+# docs
+site/
diff --git a/docs/.pages b/docs/.pages
new file mode 100644
index 000000000..276a4015a
--- /dev/null
+++ b/docs/.pages
@@ -0,0 +1,8 @@
+# Control navigation order for top-level sections
+nav:
+ - index.md
+ - User Guide
+ - Developer Guide
+ - Script API
+ - Release Notes
+ - ... # Include all other directories/files not explicitly listed
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..5675758ee
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,12 @@
+# Trilium Notes
+
+Please see the [main documentation](index.md) or visit one of our translated versions:
+
+- [Español](README.es.md)
+- [Italiano](README.it.md)
+- [日本語](README.ja.md)
+- [Русский](README.ru.md)
+- [简体中文](README-ZH_CN.md)
+- [繁體中文](README-ZH_TW.md)
+
+For the full application README, please visit our [GitHub repository](https://github.com/triliumnext/trilium).
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 000000000..5bf104216
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,94 @@
+# Trilium Notes Documentation
+
+Welcome to the official documentation for **Trilium Notes** - a hierarchical note-taking application with a focus on building large personal knowledge bases.
+
+
+
+## What is Trilium Notes?
+
+Trilium Notes is a powerful, feature-rich note-taking application designed for building and managing extensive personal knowledge bases. It offers:
+
+- **Hierarchical organization** with unlimited nesting of notes
+- **Rich text editing** with markdown support
+- **Powerful search** capabilities
+- **Note relations** and attributes for semantic connections
+- **Scripting support** for automation and customization
+- **Synchronization** between devices
+- **Encryption** for sensitive notes
+- **Web clipper** for saving web content
+
+## Quick Links
+
+
+
+- :material-rocket-launch-outline: **[Quick Start Guide](User%20Guide/quick-start.md)**
+
+ Get up and running with Trilium in minutes
+
+- :material-download: **[Installation](User%20Guide/installation.md)**
+
+ Download and install Trilium on your platform
+
+- :material-docker: **[Docker Setup](User%20Guide/docker.md)**
+
+ Deploy Trilium using Docker containers
+
+- :material-book-open-variant: **[User Guide](User%20Guide/index.md)**
+
+ Comprehensive guide to all features
+
+- :material-code-braces: **[Script API](Script%20API/index.md)**
+
+ Automate and extend Trilium with scripting
+
+- :material-wrench: **[Developer Guide](Developer%20Guide/index.md)**
+
+ Contributing and development documentation
+
+
+
+## Features Overview
+
+### Note Organization
+- Create unlimited hierarchical note structures
+- Clone notes to appear in multiple locations
+- Use attributes and relations for metadata
+- Template system for consistent note creation
+
+### Content Types
+- **Text notes** with rich formatting
+- **Code notes** with syntax highlighting
+- **Canvas notes** for drawing and diagrams
+- **File attachments** of any type
+- **Web view** for embedded content
+- **Mermaid diagrams** support
+
+### Advanced Features
+- **Full-text search** with advanced operators
+- **Note map** visualization
+- **Day notes** for journaling
+- **Book notes** for long-form content
+- **Protected notes** with encryption
+- **Note versioning** and history
+
+### Automation & Integration
+- JavaScript-based scripting
+- Custom widgets and themes
+- REST API for external integrations
+- Web clipper browser extension
+- Import/export in multiple formats
+
+## Getting Help
+
+- **[FAQ](support/faq.md)** - Frequently asked questions
+- **[Troubleshooting](support/troubleshooting.md)** - Common issues and solutions
+- **[Community Forum](https://github.com/triliumnext/trilium/discussions)** - Ask questions and share tips
+- **[Issue Tracker](https://github.com/triliumnext/trilium/issues)** - Report bugs and request features
+
+## Contributing
+
+Trilium is open-source and welcomes contributions! Check out our [Contributing Guide](Developer%20Guide/contributing.md) to get started.
+
+## License
+
+Trilium Notes is licensed under [AGPL-3.0](https://github.com/triliumnext/trilium/blob/master/LICENSE).
\ No newline at end of file
diff --git a/docs/javascripts/extra.js b/docs/javascripts/extra.js
new file mode 100644
index 000000000..f5c075755
--- /dev/null
+++ b/docs/javascripts/extra.js
@@ -0,0 +1,111 @@
+// Custom JavaScript for Trilium Notes documentation
+
+// Add smooth scrolling for anchor links
+document.addEventListener('DOMContentLoaded', function() {
+ // Smooth scroll for internal links
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function (e) {
+ e.preventDefault();
+ const target = document.querySelector(this.getAttribute('href'));
+ if (target) {
+ target.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ }
+ });
+ });
+
+ // Add copy button to code blocks if not already present
+ const codeBlocks = document.querySelectorAll('pre code');
+ codeBlocks.forEach(block => {
+ if (!block.parentElement.querySelector('.copy-button')) {
+ const button = document.createElement('button');
+ button.className = 'copy-button';
+ button.textContent = 'Copy';
+ button.addEventListener('click', () => {
+ navigator.clipboard.writeText(block.textContent);
+ button.textContent = 'Copied!';
+ setTimeout(() => {
+ button.textContent = 'Copy';
+ }, 2000);
+ });
+ block.parentElement.appendChild(button);
+ }
+ });
+
+ // Add external link indicators
+ document.querySelectorAll('a[href^="http"]').forEach(link => {
+ if (!link.hostname.includes('trilium')) {
+ link.classList.add('external-link');
+ link.setAttribute('target', '_blank');
+ link.setAttribute('rel', 'noopener noreferrer');
+ }
+ });
+
+ // Platform detection for download buttons
+ const platform = detectPlatform();
+ const downloadButtons = document.querySelectorAll('.download-button');
+ downloadButtons.forEach(button => {
+ if (button.dataset.platform === platform) {
+ button.classList.add('recommended');
+ button.innerHTML += ' Recommended';
+ }
+ });
+});
+
+// Detect user's platform
+function detectPlatform() {
+ const userAgent = navigator.userAgent.toLowerCase();
+ if (userAgent.includes('win')) return 'windows';
+ if (userAgent.includes('mac')) return 'macos';
+ if (userAgent.includes('linux')) return 'linux';
+ return 'unknown';
+}
+
+// Add search shortcuts
+document.addEventListener('keydown', function(e) {
+ // Ctrl/Cmd + K to focus search
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ const searchInput = document.querySelector('.md-search__input');
+ if (searchInput) {
+ searchInput.focus();
+ }
+ }
+});
+
+// Version selector enhancement
+const versionSelector = document.querySelector('.md-version__current');
+if (versionSelector) {
+ // Add version comparison tooltip
+ versionSelector.addEventListener('mouseenter', function() {
+ const tooltip = document.createElement('div');
+ tooltip.className = 'version-tooltip';
+ tooltip.textContent = 'Click to view other versions';
+ this.appendChild(tooltip);
+ });
+}
+
+// Analytics event tracking for documentation
+if (typeof gtag !== 'undefined') {
+ // Track external link clicks
+ document.querySelectorAll('a[href^="http"]').forEach(link => {
+ link.addEventListener('click', () => {
+ gtag('event', 'click', {
+ 'event_category': 'external_link',
+ 'event_label': link.href
+ });
+ });
+ });
+
+ // Track code copy events
+ document.querySelectorAll('.copy-button').forEach(button => {
+ button.addEventListener('click', () => {
+ gtag('event', 'copy_code', {
+ 'event_category': 'engagement',
+ 'event_label': window.location.pathname
+ });
+ });
+ });
+}
\ No newline at end of file
diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js
new file mode 100644
index 000000000..33ea4b928
--- /dev/null
+++ b/docs/javascripts/mathjax.js
@@ -0,0 +1,13 @@
+// MathJax configuration for mathematical notation support
+window.MathJax = {
+ tex: {
+ inlineMath: [['$', '$'], ['\\(', '\\)']],
+ displayMath: [['$$', '$$'], ['\\[', '\\]']],
+ processEscapes: true,
+ processEnvironments: true
+ },
+ options: {
+ ignoreHtmlClass: 'no-mathjax',
+ processHtmlClass: 'mathjax'
+ }
+};
\ No newline at end of file
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 000000000..354beb509
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,121 @@
+/* Custom styles for Trilium Notes documentation */
+
+/* Grid cards for homepage */
+.md-typeset .grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
+ margin-top: 1rem;
+}
+
+.md-typeset .grid.cards > ul {
+ display: contents;
+}
+
+.md-typeset .grid.cards > ul > li {
+ border: 1px solid var(--md-default-fg-color--lightest);
+ border-radius: .25rem;
+ display: flex;
+ flex-direction: column;
+ padding: 1rem;
+ transition: border-color .25s, box-shadow .25s;
+}
+
+.md-typeset .grid.cards > ul > li:hover {
+ border-color: var(--md-accent-fg-color);
+ box-shadow: 0 0 0 .1rem var(--md-accent-fg-color--transparent);
+}
+
+/* Improve code block appearance */
+.md-typeset pre > code {
+ font-size: .85rem;
+}
+
+/* Better admonition spacing */
+.md-typeset .admonition {
+ margin: 1.5rem 0;
+}
+
+/* Trilium brand colors */
+:root {
+ --trilium-primary: #4a5568;
+ --trilium-accent: #805ad5;
+}
+
+/* Custom badge styles */
+.badge {
+ background-color: var(--md-accent-fg-color);
+ border-radius: .125rem;
+ color: var(--md-accent-bg-color);
+ display: inline-block;
+ font-size: .75rem;
+ font-weight: 700;
+ padding: .125rem .375rem;
+ text-transform: uppercase;
+}
+
+/* Version badge */
+.version-badge {
+ background-color: var(--md-primary-fg-color);
+ margin-left: .5rem;
+}
+
+/* Platform badges */
+.platform-badge {
+ margin: 0 .25rem;
+}
+
+.platform-badge.windows {
+ background-color: #0078d4;
+}
+
+.platform-badge.macos {
+ background-color: #000000;
+}
+
+.platform-badge.linux {
+ background-color: #fcc624;
+ color: #000000;
+}
+
+/* Improve table readability */
+.md-typeset table:not([class]) {
+ font-size: .85rem;
+}
+
+.md-typeset table:not([class]) th {
+ background-color: var(--md-default-bg-color);
+ font-weight: 700;
+}
+
+/* API reference styling */
+.api-method {
+ background-color: var(--md-code-bg-color);
+ border-radius: .125rem;
+ font-family: var(--md-code-font-family);
+ font-weight: 600;
+ padding: .125rem .25rem;
+}
+
+.api-method.get {
+ color: #10b981;
+}
+
+.api-method.post {
+ color: #3b82f6;
+}
+
+.api-method.put {
+ color: #f59e0b;
+}
+
+.api-method.delete {
+ color: #ef4444;
+}
+
+/* Responsive improvements */
+@media screen and (max-width: 76.1875em) {
+ .md-typeset .grid {
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
+ }
+}
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 000000000..3d787e9ed
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,191 @@
+# MkDocs configuration for Trilium Notes documentation
+site_name: Trilium Notes Documentation
+site_url: https://docs.triliumnext.com
+site_description: Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases
+site_author: Trilium Notes Team
+
+# Repository information
+repo_name: triliumnext/trilium
+repo_url: https://github.com/triliumnext/trilium
+edit_uri: edit/main/docs/
+
+# Copyright
+copyright: Copyright © 2025 Trilium Notes
+
+# Use document-style URLs to fix image paths
+use_directory_urls: false
+
+# Theme configuration
+theme:
+ name: material
+
+ # Color scheme
+ palette:
+ # Light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: indigo
+ accent: deep-purple
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+
+ # Dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: blue-grey
+ accent: deep-purple
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+
+ # Font configuration
+ font:
+ text: Inter
+ code: JetBrains Mono
+
+ # Features
+ features:
+ - announce.dismiss
+ - content.action.edit
+ - content.action.view
+ - content.code.annotate
+ - content.code.copy
+ - content.tooltips
+ - navigation.footer
+ - navigation.indexes
+ - navigation.instant
+ - navigation.instant.prefetch
+ - navigation.instant.progress
+ - navigation.path
+ - navigation.prune
+ - navigation.sections
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.top
+ - navigation.tracking
+ - search.highlight
+ - search.share
+ - search.suggest
+ - toc.follow
+ - toc.integrate
+
+ # Icons
+ icon:
+ logo: material/note-multiple
+ repo: fontawesome/brands/github
+
+# Plugins
+plugins:
+ - search:
+ separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
+ lang:
+ - en
+ - awesome-pages:
+ collapse_single_pages: false
+ strict: false
+ order: asc
+ sort_type: natural
+ order_by: title
+ - minify:
+ minify_html: true
+ minify_js: true
+ minify_css: true
+ htmlmin_opts:
+ remove_comments: true
+ - git-revision-date-localized:
+ enable_creation_date: true
+ type: iso_datetime
+ fallback_to_build_date: true
+
+# Extensions
+markdown_extensions:
+ # Python Markdown
+ - abbr
+ - admonition
+ - attr_list
+ - def_list
+ - footnotes
+ - md_in_html
+ - toc:
+ permalink: true
+ permalink_title: Anchor link to this section for reference
+
+ # Python Markdown Extensions
+ - pymdownx.arithmatex:
+ generic: true
+ - pymdownx.betterem:
+ smart_enable: all
+ - pymdownx.caret
+ - pymdownx.details
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.keys
+ - pymdownx.mark
+ - pymdownx.smartsymbols
+ - pymdownx.snippets
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - pymdownx.tabbed:
+ alternate_style: true
+ combine_header_slug: true
+ - pymdownx.tasklist:
+ custom_checkbox: true
+ - pymdownx.tilde
+
+# Extra CSS and JavaScript (if needed)
+extra_css:
+ - stylesheets/extra.css
+
+extra_javascript:
+ - javascripts/extra.js
+ # MathJax for mathematical notation
+ - javascripts/mathjax.js
+ - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js
+
+# Extra configuration
+extra:
+ # Social links
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/triliumnext/trilium
+ - icon: fontawesome/brands/docker
+ link: https://hub.docker.com/r/triliumnext/trilium
+ - icon: fontawesome/solid/globe
+ link: https://trilium.cc
+
+ # Analytics (optional - add your own if needed)
+ analytics:
+ provider: google
+ property: G-XXXXXXXXXX # Replace with your Google Analytics ID
+ feedback:
+ title: Was this page helpful?
+ ratings:
+ - icon: material/emoticon-happy-outline
+ name: This page was helpful
+ data: 1
+ note: >-
+ Thanks for your feedback!
+ - icon: material/emoticon-sad-outline
+ name: This page could be improved
+ data: 0
+ note: >-
+ Thanks for your feedback! Help us improve this page by
+ opening an issue.
+
+ # Version
+ version:
+ provider: mike
+ default: stable
+
+# Navigation is automatically generated from folder structure by awesome-pages plugin
+# To customize order or titles, create .pages files in directories
diff --git a/package.json b/package.json
index 6eafcf347..5b3d3c3f1 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"chore:generate-openapi": "tsx ./scripts/generate-openapi.ts",
"chore:update-build-info": "tsx ./scripts/update-build-info.ts",
"chore:update-version": "tsx ./scripts/update-version.ts",
+ "chore:fix-mkdocs-structure": "tsx ./scripts/fix-mkdocs-structure.ts",
"edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs",
"edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo",
"test:all": "pnpm test:parallel && pnpm test:sequential",
diff --git a/requirements-docs.txt b/requirements-docs.txt
new file mode 100644
index 000000000..a7fa25aec
--- /dev/null
+++ b/requirements-docs.txt
@@ -0,0 +1,21 @@
+# MkDocs and Material theme requirements for Trilium documentation
+mkdocs>=1.6.0
+mkdocs-material>=9.5.0
+mkdocs-material-extensions>=1.3.0
+
+# Essential plugins
+mkdocs-awesome-pages-plugin>=2.9.0 # Auto-generate navigation from folder structure
+mkdocs-minify-plugin>=0.8.0
+mkdocs-git-revision-date-localized-plugin>=1.2.0
+
+# Optional but recommended plugins
+mkdocs-redirects>=1.2.0
+mkdocs-rss-plugin>=1.12.0
+mkdocs-glightbox>=0.3.0
+
+# For advanced features
+pillow>=10.0.0 # For social cards generation
+cairosvg>=2.7.0 # For social cards with SVG support
+
+# Search enhancements
+mkdocs-material[imaging]>=9.5.0
\ No newline at end of file
diff --git a/scripts/fix-mkdocs-structure.ts b/scripts/fix-mkdocs-structure.ts
new file mode 100644
index 000000000..e0df764d6
--- /dev/null
+++ b/scripts/fix-mkdocs-structure.ts
@@ -0,0 +1,308 @@
+#!/usr/bin/env node
+/**
+ * Fix MkDocs structure by moving overview pages to index.md inside their directories.
+ * This prevents duplicate navigation entries when a file and directory have the same name.
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+
+interface FixResult {
+ message: string;
+}
+
+/**
+ * Find markdown files that have a corresponding directory with the same name,
+ * and move them to index.md inside that directory.
+ */
+function fixDuplicateEntries(docsDir: string): FixResult[] {
+ const fixesMade: FixResult[] = [];
+
+ function walkDir(dir: string): void {
+ let files: string[];
+ try {
+ files = fs.readdirSync(dir);
+ } catch (err) {
+ console.warn(`Warning: Unable to read directory ${dir}: ${err.message}`);
+ return;
+ }
+
+ for (const file of files) {
+ const filePath = path.join(dir, file);
+ let stat: fs.Stats;
+
+ try {
+ stat = fs.statSync(filePath);
+ } catch (err) {
+ // File might have been moved already, skip it
+ continue;
+ }
+
+ if (stat.isDirectory()) {
+ walkDir(filePath);
+ } else if (file.endsWith('.md')) {
+ const basename = file.slice(0, -3); // Remove .md extension
+ const dirPath = path.join(dir, basename);
+
+ // Check if there's a directory with the same name
+ if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
+ const indexPath = path.join(dirPath, 'index.md');
+
+ // Check if index.md already exists in that directory
+ if (!fs.existsSync(indexPath)) {
+ // Move the file to index.md in the directory
+ fs.renameSync(filePath, indexPath);
+ fixesMade.push({
+ message: `Moved ${path.relative(docsDir, filePath)} -> ${path.relative(docsDir, indexPath)}`
+ });
+
+ // Move associated images with pattern basename_*
+ try {
+ const dirFiles = fs.readdirSync(dir);
+ for (const imgFile of dirFiles) {
+ if (imgFile.startsWith(`${basename}_`)) {
+ const imgSrc = path.join(dir, imgFile);
+ try {
+ if (!fs.statSync(imgSrc).isDirectory()) {
+ const imgDest = path.join(dirPath, imgFile);
+ fs.renameSync(imgSrc, imgDest);
+ fixesMade.push({
+ message: `Moved ${path.relative(docsDir, imgSrc)} -> ${path.relative(docsDir, imgDest)}`
+ });
+ }
+ } catch (err) {
+ // File might have been moved already, skip it
+ }
+ }
+ }
+ } catch (err) {
+ // Directory might not exist anymore, skip it
+ }
+
+ // Move exact match images
+ const imgExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg'];
+ for (const ext of imgExtensions) {
+ const imgFile = path.join(dir, `${basename}${ext}`);
+ if (fs.existsSync(imgFile)) {
+ const imgDest = path.join(dirPath, `${basename}${ext}`);
+ fs.renameSync(imgFile, imgDest);
+ fixesMade.push({
+ message: `Moved ${path.relative(docsDir, imgFile)} -> ${path.relative(docsDir, imgDest)}`
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ walkDir(docsDir);
+ return fixesMade;
+}
+
+/**
+ * Update references in markdown files to point to the new locations.
+ */
+function updateReferences(docsDir: string): FixResult[] {
+ const updatesMade: FixResult[] = [];
+
+ function fixLink(match: string, text: string, link: string, currentDir: string, isIndex: boolean): string {
+ // Skip external links
+ if (link.startsWith('http')) {
+ return match;
+ }
+
+ // Decode URL-encoded paths for processing
+ // Use decodeURIComponent which is equivalent to Python's unquote
+ let decodedLink: string;
+ try {
+ decodedLink = decodeURIComponent(link);
+ } catch (err) {
+ // If decoding fails, use the original link
+ decodedLink = link;
+ }
+
+ // Special case: if we're in index.md and the link starts with the parent directory name
+ if (isIndex && decodedLink.includes('/')) {
+ const pathParts = decodedLink.split('/');
+ const parentDirName = path.basename(currentDir);
+
+ // Check if first part matches the parent directory name
+ if (pathParts[0] === parentDirName) {
+ // This is a self-referential path, strip the first part
+ const fixedLink = pathParts.slice(1).join('/');
+ // Continue processing with the fixed link
+ const decodedFixedLink = fixedLink;
+
+ // Check if this fixed link points to a directory with index.md
+ if (!decodedFixedLink.startsWith('/')) {
+ const resolvedPath = path.resolve(currentDir, decodedFixedLink);
+
+ if (resolvedPath.endsWith('.md')) {
+ const potentialDir = resolvedPath.slice(0, -3);
+ const potentialIndex = path.join(potentialDir, 'index.md');
+
+ if (fs.existsSync(potentialIndex)) {
+ // Check if they share the same parent directory
+ if (path.dirname(potentialDir) === path.dirname(currentDir)) {
+ // It's a sibling - just use directory name
+ const dirName = path.basename(potentialDir).replace(/ /g, '%20');
+ return `[${text}](${dirName}/)`;
+ }
+
+ // Calculate relative path from current file to the directory
+ const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20');
+ return `[${text}](${newPath}/)`;
+ }
+ }
+ }
+
+ // If no special handling needed for the fixed link, return it as-is
+ const fixedLinkEncoded = fixedLink.replace(/ /g, '%20');
+ return `[${text}](${fixedLinkEncoded})`;
+ }
+ }
+
+ // For any .md link, check if there's a directory with index.md
+ if (!decodedLink.startsWith('/')) {
+ const resolvedPath = path.resolve(currentDir, decodedLink);
+
+ // Check if this points to a file that should be a directory
+ if (resolvedPath.endsWith('.md')) {
+ const potentialDir = resolvedPath.slice(0, -3);
+ const potentialIndex = path.join(potentialDir, 'index.md');
+
+ // If a directory with index.md exists, update the link
+ if (fs.existsSync(potentialIndex)) {
+ if (isIndex) {
+ // Check if they share the same parent directory
+ if (path.dirname(potentialDir) === path.dirname(currentDir)) {
+ // It's a sibling - just use directory name
+ const dirName = path.basename(potentialDir).replace(/ /g, '%20');
+ return `[${text}](${dirName}/)`;
+ }
+ }
+
+ // Calculate relative path from current file to the directory
+ const newPath = path.relative(currentDir, potentialDir).replace(/\\/g, '/').replace(/ /g, '%20');
+ return `[${text}](${newPath}/)`;
+ }
+ }
+ }
+
+ // Also handle local references (same directory) - should be 'if', not 'elif'
+ // This is intentional to handle both absolute and relative paths
+ if (!decodedLink.includes('/')) {
+ const basename = decodedLink.slice(0, -3); // Remove .md
+ const possibleDir = path.join(currentDir, basename);
+
+ if (fs.existsSync(possibleDir) && fs.statSync(possibleDir).isDirectory()) {
+ const encodedBasename = basename.replace(/ /g, '%20');
+ return `[${text}](${encodedBasename}/)`;
+ }
+ }
+
+ return match;
+ }
+
+ function walkDir(dir: string): void {
+ let files: string[];
+ try {
+ files = fs.readdirSync(dir);
+ } catch (err) {
+ console.warn(`Warning: Unable to read directory ${dir}: ${err.message}`);
+ return;
+ }
+
+ for (const file of files) {
+ const filePath = path.join(dir, file);
+ let stat: fs.Stats;
+
+ try {
+ stat = fs.statSync(filePath);
+ } catch (err) {
+ // File might have been moved already, skip it
+ continue;
+ }
+
+ if (stat.isDirectory()) {
+ walkDir(filePath);
+ } else if (file.endsWith('.md')) {
+ let content = fs.readFileSync(filePath, 'utf-8');
+ const originalContent = content;
+
+ const isIndex = file === 'index.md';
+ const currentDir = path.dirname(filePath);
+
+ // Update markdown links: [text](path.md)
+ const pattern = /\[([^\]]*)\]\(([^)]+\.md)\)/g;
+ content = content.replace(pattern, (match, text, link) => {
+ return fixLink(match, text, link, currentDir, isIndex);
+ });
+
+ if (content !== originalContent) {
+ fs.writeFileSync(filePath, content, 'utf-8');
+ updatesMade.push({
+ message: `Updated references in ${path.relative(docsDir, filePath)}`
+ });
+ }
+ }
+ }
+ }
+
+ walkDir(docsDir);
+ return updatesMade;
+}
+
+function main(): number {
+ // Get the docs directory
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
+ const projectRoot = path.dirname(scriptDir);
+ const docsDir = path.join(projectRoot, 'docs');
+
+ // Handle Windows paths (remove leading slash if on Windows)
+ const normalizedDocsDir = process.platform === 'win32' && docsDir.startsWith('/')
+ ? docsDir.substring(1)
+ : docsDir;
+
+ if (!fs.existsSync(normalizedDocsDir)) {
+ console.error(`Error: docs directory not found at ${normalizedDocsDir}`);
+ return 1;
+ }
+
+ console.log(`Fixing MkDocs structure in ${normalizedDocsDir}`);
+ console.log('-'.repeat(50));
+
+ // Fix duplicate entries
+ const fixes = fixDuplicateEntries(normalizedDocsDir);
+ if (fixes.length > 0) {
+ console.log('Files reorganized:');
+ for (const fix of fixes) {
+ console.log(` - ${fix.message}`);
+ }
+ } else {
+ console.log('No duplicate entries found that need fixing');
+ }
+
+ console.log();
+
+ // Update references
+ const updates = updateReferences(normalizedDocsDir);
+ if (updates.length > 0) {
+ console.log('References updated:');
+ for (const update of updates) {
+ console.log(` - ${update.message}`);
+ }
+ } else {
+ console.log('No references needed updating');
+ }
+
+ console.log('-'.repeat(50));
+ console.log(`Structure fix complete: ${fixes.length} files moved, ${updates.length} files updated`);
+
+ return 0;
+}
+
+// Run the main function
+process.exit(main());
\ No newline at end of file