obsidian-reference-architecture
Offers a modular reference structure for building complex, maintainable Obsidian plugins.
Install
mkdir -p .claude/skills/obsidian-reference-architecture && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/7445" && unzip -o skill.zip -d .claude/skills/obsidian-reference-architecture && rm skill.zipInstalls to .claude/skills/obsidian-reference-architecture
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Implement Obsidian reference architecture with best-practice projectKey capabilities
- →Define a modular project structure for Obsidian plugins
- →Separate concerns into views, commands, settings, and services
- →Manage plugin state
- →Implement a service layer for vault operations
- →Register commands and manage views
- →Scope CSS to prevent conflicts
How it works
This skill defines a modular project structure for Obsidian plugins, separating concerns into distinct directories for commands, views, settings, and services. It provides code examples for a thin main entry point, command registration, and view management.
Inputs & outputs
When to use obsidian-reference-architecture
- →Designing new Obsidian plugins
- →Refactoring existing plugin structure
- →Implementing modular plugin services
- →Standardizing plugin UI and commands
About this skill
Obsidian Reference Architecture
Overview
Architecture patterns for complex Obsidian plugins: modular project structure with separate files for views, commands, settings, and services; state management; a service layer for vault operations; command registry; view manager; and CSS scoping.
Prerequisites
- TypeScript and Obsidian API familiarity
- Working build pipeline (esbuild recommended)
- Plugin scaffolding complete (
manifest.json,package.json,tsconfig.json)
Instructions
Step 1: Project Structure
my-plugin/
├── src/
│ ├── main.ts # Plugin entry — thin orchestrator
│ ├── types.ts # Shared interfaces and type definitions
│ ├── constants.ts # Plugin-wide constants
│ ├── commands/
│ │ ├── index.ts # Command registry
│ │ ├── insert-template.ts
│ │ └── toggle-sidebar.ts
│ ├── views/
│ │ ├── index.ts # View registry
│ │ ├── sidebar-view.ts
│ │ └── modal-view.ts
│ ├── settings/
│ │ ├── settings.ts # Settings interface and defaults
│ │ └── settings-tab.ts # Settings UI tab
│ └── services/
│ ├── vault-service.ts # File read/write/search operations
│ ├── metadata-service.ts # Frontmatter and cache operations
│ └── sync-service.ts # External sync or background tasks
├── styles.css
├── manifest.json
├── versions.json
├── package.json
├── tsconfig.json
└── esbuild.config.mjs
Step 2: Thin Main Entry Point
// src/main.ts — orchestrates, does not implement
import { Plugin } from 'obsidian';
import { MyPluginSettings, DEFAULT_SETTINGS } from './settings/settings';
import { MySettingTab } from './settings/settings-tab';
import { registerCommands } from './commands';
import { registerViews } from './views';
import { VaultService } from './services/vault-service';
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
vaultService: VaultService;
async onload() {
await this.loadSettings();
this.vaultService = new VaultService(this.app);
registerCommands(this);
registerViews(this);
this.addSettingTab(new MySettingTab(this.app, this));
}
onunload() {
// Services clean up their own resources
this.vaultService.destroy();
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
Step 3: Command Registry Pattern
// src/commands/index.ts
import type MyPlugin from '../main';
import { insertTemplate } from './insert-template';
import { toggleSidebar } from './toggle-sidebar';
export function registerCommands(plugin: MyPlugin) {
plugin.addCommand({
id: 'insert-template',
name: 'Insert Template',
editorCallback: (editor, view) => insertTemplate(plugin, editor, view),
});
plugin.addCommand({
id: 'toggle-sidebar',
name: 'Toggle Sidebar',
callback: () => toggleSidebar(plugin),
});
}
// src/commands/insert-template.ts
import { Editor, MarkdownView } from 'obsidian';
import type MyPlugin from '../main';
export function insertTemplate(plugin: MyPlugin, editor: Editor, view: MarkdownView) {
const template = plugin.settings.defaultTemplate;
editor.replaceSelection(template);
}
Step 4: View Manager
// src/views/index.ts
import type MyPlugin from '../main';
import { SidebarView, VIEW_TYPE_SIDEBAR } from './sidebar-view';
export function registerViews(plugin: MyPlugin) {
plugin.registerView(VIEW_TYPE_SIDEBAR, (leaf) => new SidebarView(leaf, plugin));
// Add ribbon icon to activate view
plugin.addRibbonIcon('layout-sidebar-right', 'Open Sidebar', () => {
activateView(plugin);
});
}
async function activateView(plugin: MyPlugin) {
const { workspace } = plugin.app;
let leaf = workspace.getLeavesOfType(VIEW_TYPE_SIDEBAR)[0];
if (!leaf) {
const rightLeaf = workspace.getRightLeaf(false);
if (rightLeaf) {
await rightLeaf.setViewState({ type: VIEW_TYPE_SIDEBAR, active: true });
leaf = rightLeaf;
}
}
if (leaf) {
workspace.revealLeaf(leaf);
}
}
// src/views/sidebar-view.ts
import { ItemView, WorkspaceLeaf } from 'obsidian';
import type MyPlugin from '../main';
export const VIEW_TYPE_SIDEBAR = 'my-plugin-sidebar';
export class SidebarView extends ItemView {
plugin: MyPlugin;
constructor(leaf: WorkspaceLeaf, plugin: MyPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return VIEW_TYPE_SIDEBAR;
}
getDisplayText(): string {
return 'My Plugin';
}
getIcon(): string {
return 'layout-sidebar-right';
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
container.addClass('my-plugin-sidebar');
container.createEl('h3', { text: 'My Plugin' });
const list = container.createEl('ul');
// Populate from service layer
const files = await this.plugin.vaultService.getRecentFiles(10);
for (const file of files) {
list.createEl('li', { text: file.basename });
}
}
async onClose() {
// Clean up DOM references
this.containerEl.empty();
}
}
Step 5: Service Layer for Vault Operations
// src/services/vault-service.ts
import { App, TFile, TFolder, CachedMetadata } from 'obsidian';
export class VaultService {
constructor(private app: App) {}
// File operations
async readFile(path: string): Promise<string> {
const file = this.app.vault.getAbstractFileByPath(path);
if (!(file instanceof TFile)) throw new Error(`Not a file: ${path}`);
return this.app.vault.read(file);
}
async writeFile(path: string, content: string): Promise<void> {
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
await this.app.vault.modify(file, content);
} else {
await this.app.vault.create(path, content);
}
}
// Search operations
getFilesInFolder(folderPath: string): TFile[] {
const folder = this.app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) return [];
return folder.children.filter((f): f is TFile => f instanceof TFile);
}
getRecentFiles(limit: number): TFile[] {
return this.app.vault.getMarkdownFiles()
.sort((a, b) => b.stat.mtime - a.stat.mtime)
.slice(0, limit);
}
// Metadata operations
getMetadata(file: TFile): CachedMetadata | null {
return this.app.metadataCache.getFileCache(file);
}
getFrontmatter(file: TFile): Record<string, unknown> | undefined {
return this.getMetadata(file)?.frontmatter;
}
// Cleanup
destroy() {
// Release any held references
}
}
Step 6: Settings Architecture
// src/settings/settings.ts
export interface MyPluginSettings {
version: number;
defaultTemplate: string;
showStatusBar: boolean;
syncInterval: number;
}
export const DEFAULT_SETTINGS: MyPluginSettings = {
version: 1,
defaultTemplate: '## New Section\n\n',
showStatusBar: true,
syncInterval: 300,
};
// src/settings/settings-tab.ts
import { App, PluginSettingTab, Setting } from 'obsidian';
import type MyPlugin from '../main';
export class MySettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Default template')
.setDesc('Content inserted by the Insert Template command')
.addTextArea(text => text
.setValue(this.plugin.settings.defaultTemplate)
.onChange(async (value) => {
this.plugin.settings.defaultTemplate = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Show status bar')
.setDesc('Display plugin status in the bottom bar')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showStatusBar)
.onChange(async (value) => {
this.plugin.settings.showStatusBar = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Sync interval')
.setDesc('Seconds between background syncs (0 to disable)')
.addSlider(slider => slider
.setLimits(0, 3600, 60)
.setValue(this.plugin.settings.syncInterval)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.syncInterval = value;
await this.plugin.saveSettings();
}));
}
}
Step 7: CSS Architecture with Plugin-Scoped Classes
/* styles.css — all classes prefixed with plugin id */
/* Layout */
.my-plugin-sidebar {
padding: 8px 12px;
}
.my-plugin-sidebar h3 {
margin: 0 0 12px;
font-size: var(--font-ui-medium);
color: var(--text-normal);
}
.my-plugin-sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
.my-plugin-sidebar li {
padding: 4px 8px;
border-radius: var(--radius-s);
cursor: pointer;
color: var(--text-muted);
}
.my-plugin-sidebar li:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
}
/* Modal styles */
.my-plugin-modal .modal-content {
padding: 16px;
}
/* Use Obsidian CSS variables — never hardcode colors */
.my-plugin-highlight {
background: var(--background-modifier-success);
color: var(--text-on-accent);
border-radius: var(--radius-s);
padding: 2px 6px;
}
/* Responsive: Obsidian handles mobile layout, but adjust spacing */
.is-mobile .my-plugin-sidebar {
padding: 4px 8px;
}
.is-mobile .my-plugin-sidebar li {
padding: 8px; /* Larger touch targets */
}
Key CSS rules:
- Prefix every class with your plugin id to avoid collisions
- Use Obsidian CSS variables (
--text-normal,--background-modifier-hover, etc.) for theme com
Content truncated.
Prerequisites
How it compares
This approach organizes complex Obsidian plugins into a modular structure, unlike a single-file or less organized plugin implementation.
Compared to similar skills
obsidian-reference-architecture side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| obsidian-reference-architecture (this skill) | 1 | 27d | No flags | Intermediate |
| deepwiki-rs | 25 | 9mo | Review | Intermediate |
| technology-stack | 3 | 3mo | No flags | Intermediate |
| vercel-reference-architecture | 3 | 27d | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by jeremylongshore
View all by jeremylongshore →You might also like
deepwiki-rs
sopaco
AI-powered Rust documentation generation engine for comprehensive codebase analysis, C4 architecture diagrams, and automated technical documentation. Use when Claude needs to analyze source code, understand software architecture, generate technical specs, or create professional documentation from any programming language.
technology-stack
ag-grid
Technology choices and architectural constraints for AG Grid. Use when choosing technologies, adding dependencies, or understanding zero-dependency requirements.
vercel-reference-architecture
jeremylongshore
Implement Vercel reference architecture with best-practice project layout. Use when designing new Vercel integrations, reviewing project structure, or establishing architecture standards for Vercel applications. Trigger with phrases like "vercel architecture", "vercel best practices", "vercel project structure", "how to organize vercel", "vercel layout".
project-principles
vm0-ai
Core architectural and code quality principles that guide all development decisions in the vm0 project
tldr-code
parcadei
Token-efficient code analysis via 5-layer stack (AST, Call Graph, CFG, DFG, PDG). 95% token savings.
Feature Development
RunAnyDev
Guidelines and best practices for creating new features in the application.