607 lines
13 KiB
Markdown
607 lines
13 KiB
Markdown
# 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
|
|
<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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```bash
|
|
# 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
|