# 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 enable - `hotkeys.json` - Keyboard shortcuts - `plugins/obsidian-git/data.json` - Git plugin configuration **Attachment Folder:** ```json { "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 cache - `graph.json` - Graph view state **Shared (Committed):** - `app.json` - Core settings - `community-plugins.json` - Plugin list - `hotkeys.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 ```typescript // 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:** ```typescript 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 ```typescript // 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` ```typescript 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` ```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:** 1. **User A** edits article, commits, pushes 2. **User B** starts Obsidian → auto-pulls User A's changes 3. **User B** edits different article → no conflict 4. **User B** commits & pushes 5. **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`) ```bash # 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`) ```bash # 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`) ```bash # 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 ```yaml # ✅ 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):** ```typescript // 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:** ```yaml # 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:** ```bash # 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/` ```bash # 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 ```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: ```vue