wiki_ghostguild/docs/TECHNICAL_ARCHITECTURE.md

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 enable
  • hotkeys.json - Keyboard shortcuts
  • plugins/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 cache
  • graph.json - Graph view state

Shared (Committed):

  • app.json - Core settings
  • community-plugins.json - Plugin list
  • hotkeys.json - Team shortcuts

Location: /app/server/plugins/wikilink-transform.ts

Hook: content:file:beforeParse

Timing: Runs during Nuxt Content processing, before markdown parsing

Transformations:

// 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:

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
![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

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:

  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)

# 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)

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

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)
![](/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

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>

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


Last Updated: November 2025

Maintainer: Wiki-GhostGuild Team