13 KiB
Technical Architecture: Obsidian-Nuxt Integration
For: Technical team members, maintainers
System Overview
Obsidian Vault (source) Nuxt Content (processing) Web (output)
↓ ↓ ↓
content/articles/*.md → Wikilink Transform Plugin → /articles/slug
with [[links]] (Nitro server plugin) with /articles/ links
with ![[images]] with /img/ references
Image embed transformation
(Nitro server plugin)
↓
Nuxt build generates
Static HTML + JSON
Architecture Components
1. Obsidian Vault Configuration
Location: /content/articles/.obsidian/
Key Files:
app.json- Settings (attachment folder, link format, etc.)community-plugins.json- List of plugins to enablehotkeys.json- Keyboard shortcutsplugins/obsidian-git/data.json- Git plugin configuration
Attachment Folder:
{
"attachmentFolderPath": "../../public/img"
}
This tells Obsidian to save images to /public/img/ instead of keeping them in the vault.
User-Specific (Gitignored):
workspace.json- Open files, window layout (per-user)appearance.json- Theme preferences (per-user)cache/- Obsidian's internal cachegraph.json- Graph view state
Shared (Committed):
app.json- Core settingscommunity-plugins.json- Plugin listhotkeys.json- Team shortcuts
2. Wikilink Transformation Plugin
Location: /app/server/plugins/wikilink-transform.ts
Hook: content:file:beforeParse
Timing: Runs during Nuxt Content processing, before markdown parsing
Transformations:
Wikilinks → Markdown Links
// Input
[[P0 - Communication Norms for Game Studios]]
[[Grant Writing Guide|Grant Guide]]
// Output
[Communication Norms for Game Studios](/articles/communication-norms-for-game-studios)
[Grant Guide](/articles/grant-writing-guide)
// Logic
1. Strip P0/P1/P2 prefix
2. Convert title to slug (lowercase, spaces→hyphens, special chars→removed)
3. Create markdown link with /articles/ prefix
Slug Generation:
function titleToSlug(title: string): string {
return title
.toLowerCase()
.replace(/['"]/g, '') // Remove quotes
.replace(/&/g, 'and') // & → and
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Spaces → hyphens
.replace(/-+/g, '-') // Collapse hyphens
.replace(/^-|-$/g, ''); // Trim edges
}
// Examples:
"Grant Writing & Funding Strategy" → "grant-writing-and-funding-strategy"
"Co-op: Structure & Governance" → "co-op-structure-and-governance"
"Q&A: Funding" → "qa-funding"
Image Embeds → Markdown Images
// Input
![[diagram.jpg]]
![[workflow.png|Team workflow diagram]]
// Output


// Logic
1. Extract filename and optional alt text
2. Create markdown image with /img/ prefix
3. Content Processing Pipeline
Sequence:
1. File read from disk
File: /content/articles/article-name.md
↓
2. content:file:beforeParse hook (OUR PLUGIN)
- Extract wikilinks: [[...]]
- Extract image embeds: ![[...]]
- Transform both to markdown syntax
- Update file.body
↓
3. Frontmatter parsing (gray-matter)
- Extract YAML frontmatter
- Validate against schema
↓
4. Markdown parsing (Nuxt Content)
- Convert markdown to AST
- Generate HTML structure
↓
5. Static generation
- Render article pages
- Create JSON payloads
- Output to .output/
No Markdown Plugins Needed:
Nuxt Content v3 uses Nuxt's built-in markdown processor. We don't need remark/rehype plugins because the Nitro plugin hook runs before all parsing.
4. Image Handling
Storage: /public/img/
Workflow:
User in Obsidian
↓
Pastes image (Cmd+V)
↓
Obsidian saves to /public/img/
↓
User writes ![[filename.jpg]]
↓
Git add + commit
↓
Image goes to repository
↓
At build:
- Plugin transforms ![[filename.jpg]] → 
- Build copies /public/img/ to output
↓
Web server serves /img/filename.jpg
Path Resolution:
When Obsidian saves image from vault at /content/articles/:
From: /content/articles/some-article.md
To: /public/img/
Relative: ../../public/img
So when you paste in Obsidian, it:
Saves to: /public/img/
Creates: 
The transformation plugin converts this to /img/image.jpg for web output.
5. Nuxt Content Collection
Configuration: content.config.ts
export default defineContentConfig({
collections: {
articles: defineCollection({
type: "data",
source: "**/*.md", // Finds all .md files
schema: z.object({
title: z.string(),
description: z.string().optional(),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
author: z.string().optional(),
contributors: z.array(z.string()).optional(),
published: z.boolean().default(true),
featured: z.boolean().default(false),
accessLevel: z.string().optional(),
publishedAt: z.string().optional(),
}),
}),
},
});
Validation:
- Frontmatter must match schema
- Missing required fields cause build errors
- All fields run through Zod validation
6. Git Workflow Integration
Via Obsidian Git Plugin
Configuration: .obsidian/plugins/obsidian-git/data.json
{
"autoPullOnBoot": true, // Pull on startup
"autoPullInterval": 5, // Pull every 5 minutes
"disablePush": false, // Allow pushing
"pullBeforePush": true, // Always pull before pushing
"conflictHandlingStrategy": "ask" // Ask on conflicts
}
User Workflow:
- User A edits article, commits, pushes
- User B starts Obsidian → auto-pulls User A's changes
- User B edits different article → no conflict
- User B commits & pushes
- User A continues working → might pull when prompted
Conflict Resolution:
If both users edit same file:
1. Pull fails with conflict
2. Obsidian Git creates conflict-files-obsidian-git.md
3. User sees conflict markers in file:
<<<<<<< HEAD
User A's version
=======
User B's version
>>>>>>> main
4. User manually resolves (chooses/merges versions)
5. User commits resolution
6. User pushes
Build Process
Development (npm run dev)
# Starts:
# 1. Nuxt dev server (hot reload)
# 2. Watches /content/articles/ for changes
# 3. When file saves: re-processes & hot-reload in browser
npm run dev
# → http://localhost:3000
# → Changes live-update without refresh
Production (npm run generate)
# Builds static site:
# 1. Processes all content files
# 2. Runs wikilink transformation
# 3. Generates HTML pages + JSON
# 4. Optimizes assets
# 5. Outputs to .output/public/
npm run generate
# → .output/public/ contains complete static site
# → Ready to deploy to CDN/hosting
Static Preview (npm run preview)
# Starts server serving built output:
# Shows what production will look like
npm run preview
# → http://localhost:3000
# → Serves from .output/public/
File Mappings
Article Files → URL Slugs:
Obsidian filename Content title URL slug
────────────────────────────────────────────────────────────────────────
communication-norms*.md Communication Norms Document /articles/communication-norms-document-for-game-studios
meeting-agenda*.md Meeting Agenda & Facilitation /articles/meeting-agenda-facilitation-guide-for-game-studios
co-op-structure*.md Co-op: Structure & Governance /articles/co-op-structure-and-governance
*Actual filename doesn't matter for slug generation
Slug comes from frontmatter title
Why this matters:
If you rename a file in Obsidian:
- ❌ Obsidian updates internal links automatically (good!)
- ✅ Slug stays same (derived from title, not filename)
- If you change the title in frontmatter: slug changes → URL changes
Performance Characteristics
Build Times
| Operation | Time | Notes |
|---|---|---|
| Dev server startup | ~10-15s | Nuxt init + content processing |
| Hot reload | ~1-2s | Single file change |
| Full static build | ~30-60s | 42 articles + optimization |
| Content indexing | ~100ms | Build-time caching |
Runtime Performance
- No runtime transformation - All done at build
- Static HTML output - Zero processing at serve time
- Fast first page load - No client-side rendering for content
Scaling
- 100 articles - Build still <60s
- 1000 articles - Might need optimization (parallel builds)
- Image count - No impact (images in /public/, not in content)
Edge Cases & Solutions
Case Sensitivity
Problem: Filenames case-sensitive on Linux, case-insensitive on Mac
Solution: Always use lowercase slugs
# ✅ Good
- [[Grant Writing Guide]]
- [[case-study-studio-xyz]]
# ❌ Problematic
- [[Grant WRITING Guide]]
- [[Case-Study-Studio-XYZ]]
Special Characters
In Article Titles:
Input: "Co-op: Structure & Governance"
Slug: "co-op-structure-and-governance"
Input: "Q&A: Funding"
Slug: "qa-funding"
Input: "The "Beginning""
Slug: "the-beginning"
In Image Names:
✅ Good: meeting-workflow-diagram.jpg
❌ Bad: Meeting Workflow Diagram.jpg (spaces)
❌ Bad: image (2).jpg (parentheses)
Circular References
Not a problem:
- Article A links to Article B
- Article B links to Article A
- Both work fine (static links, no runtime resolution)
Non-Existent Links
Behavior:
- Wikilink transforms to
/articles/non-existent-page - User clicks → sees 404 page
- No build error
Detection (optional enhancement):
// Could validate at build time:
// - Get all article slugs
// - Check each transformed link
// - Warn if any don't exist
Troubleshooting
Build Fails: "Invalid frontmatter"
Cause: File violates schema
Fix:
# Check:
- Is 'title' present?
- Is YAML syntax valid (spaces not tabs)?
- Are arrays formatted correctly?
# Example invalid:
- tags: baby-ghosts, p0 # ❌ Missing brackets
+ tags: [baby-ghosts, p0] # ✅ Correct array syntax
Wikilinks Showing Literally
Cause: Plugin didn't run or had error
Check:
# 1. Restart dev server
npm run dev
# 2. Check plugin file exists
ls app/server/plugins/wikilink-transform.ts
# 3. Check for errors in terminal
# Look for "error TS" messages
# 4. Clear cache
rm -rf .nuxt
npm run dev
Images Not Displaying
Cause 1: Image not in /public/img/
# Check:
ls public/img/image.jpg
# If missing, add it:
cp path/to/image.jpg public/img/
git add public/img/image.jpg
Cause 2: Wrong path in markdown
# Check these:
![[image.jpg]] # ✅ Correct (Obsidian syntax)
 # ✅ Correct (markdown)
 # ❌ Wrong (extra /public/)
Git Conflicts
Prevention:
- Pull before editing
- Work on different files
- Push frequently
Resolution:
- See OBSIDIAN_SETUP_GUIDE.md → "Handling Conflicts"
Future Enhancements
Backlinks Display
Show which articles link to current article:
<div class="backlinks">
<h3>Referenced by:</h3>
<ul>
<li v-for="article in backlinks">
<NuxtLink :to="`/articles/${article.slug}`">
{{ article.title }}
</NuxtLink>
</li>
</ul>
</div>
Link Validation at Build
Fail build if broken wikilinks detected:
// In plugin:
const allSlugs = /* get all article slugs */
wikilinks.forEach(link => {
const slug = wikilinkToSlug(link)
if (!allSlugs.includes(slug)) {
throw new Error(`Broken wikilink: ${link}`)
}
})
Graph Visualization
Show article connections as interactive graph:
Communication Norms
↓
/ | \
↓ ↓ ↓
M.Guide Peer Support Bylaws
Maintenance
Regular Tasks
Weekly:
- Check for broken images (audit script)
- Review commit messages (consistency)
Monthly:
- Run full build test (
npm run generate) - Test on staging server
- Backup Forgejo repository
Quarterly:
- Review plugin versions (Obsidian Git, etc.)
- Update dependencies if needed
Updating Obsidian Vault Config
Only need to commit .obsidian/ folder if config changes:
# Check what changed
git status
# If app.json changed:
git add content/articles/.obsidian/app.json
git commit -m "Update Obsidian vault settings"
git push
# Other users pull and get new settings
References
- Nuxt Content v3: https://content.nuxt.com
- Nuxt: https://nuxt.com
- Nitro: https://nitro.unjs.io
- Obsidian: https://obsidian.md
- Obsidian Git: https://github.com/Vinzent03/obsidian-git
Last Updated: November 2025
Maintainer: Wiki-GhostGuild Team