wiki_ghostguild/docs/TECHNICAL_ARCHITECTURE.md

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, spaceshyphens, special charsremoved)
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
![diagram.jpg](/img/diagram.jpg)
![Team workflow diagram](/img/workflow.png)
// 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]] → ![...](/img/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: ![image](../../public/img/image.jpg)
```
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)
![](/img/image.jpg) # ✅ Correct (markdown)
![](/public/img/image.jpg) # ❌ 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