Site de Emmanuel Demey

Building Your First Obsidian Plugin: A Word Counter Badge (Part 1)

Learn to create an Obsidian plugin from scratch. Build a real-time word counter that displays in the status bar with this practical, hands-on guide.

Obsidian is a powerful knowledge management tool that lets you extend functionality through plugins. In this tutorial, we’ll build a Word Counter Badge that displays real-time word and character counts in the status bar.

This project teaches Obsidian plugin fundamentals while creating something genuinely useful.

What We’ll Build

Our Word Counter Badge plugin will:

  • Display word count in Obsidian’s status bar
  • Update automatically when you switch notes or edit content
  • Show both word and character counts
  • Handle edge cases gracefully

Prerequisites

Before starting, ensure you have:

  • Node.js (v16 or higher) and npm installed
  • Basic TypeScript knowledge (we’ll explain as we go)
  • Obsidian installed on your computer
  • A code editor (VS Code recommended)
  • Git (optional, for cloning the sample plugin)

Understanding Obsidian Plugin Architecture

Obsidian plugins are TypeScript/JavaScript modules that interact with Obsidian’s API. They follow a specific structure:

Key Components:

  • main.ts: Your plugin’s entry point
  • manifest.json: Plugin metadata (name, version, description)
  • Plugin class: Extends Obsidian’s base Plugin class
  • Lifecycle hooks: onload() and onunload()

Setting Up Your Plugin Project

Step 1: Create the Plugin Directory

Create a new folder in your Obsidian vault’s plugins directory:

cd /path/to/your/vault/.obsidian/plugins
mkdir word-counter-badge
cd word-counter-badge

Step 2: Initialize the Project

Create a package.json:

{
  "name": "word-counter-badge",
  "version": "1.0.0",
  "description": "Displays word and character count in the status bar",
  "main": "main.js",
  "scripts": {
    "dev": "node esbuild.config.mjs",
    "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
  },
  "keywords": ["obsidian", "plugin", "word-counter"],
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^20.10.0",
    "@typescript-eslint/eslint-plugin": "^6.15.0",
    "@typescript-eslint/parser": "^6.15.0",
    "builtin-modules": "^3.3.0",
    "esbuild": "^0.19.9",
    "obsidian": "latest",
    "tslib": "^2.6.2",
    "typescript": "^5.3.3"
  }
}

Step 3: Install Dependencies

npm install

Step 4: Configure TypeScript

Create tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "inlineSourceMap": true,
    "inlineSources": true,
    "module": "ESNext",
    "target": "ES2018",
    "allowJs": true,
    "noImplicitAny": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "isolatedModules": true,
    "strictNullChecks": true,
    "lib": ["DOM", "ES5", "ES6", "ES7"],
    "skipLibCheck": true
  },
  "include": ["**/*.ts"]
}

Step 5: Create Build Configuration

Create esbuild.config.mjs:

import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";

const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
*/
`;

const prod = process.argv[2] === "production";

const context = await esbuild.context({
  banner: { js: banner },
  entryPoints: ["main.ts"],
  bundle: true,
  external: [
    "obsidian",
    "electron",
    "@codemirror/autocomplete",
    "@codemirror/collab",
    "@codemirror/commands",
    "@codemirror/language",
    "@codemirror/lint",
    "@codemirror/search",
    "@codemirror/state",
    "@codemirror/view",
    "@lezer/common",
    "@lezer/highlight",
    "@lezer/lr",
    ...builtins,
  ],
  format: "cjs",
  target: "es2018",
  logLevel: "info",
  sourcemap: prod ? false : "inline",
  treeShaking: true,
  outfile: "main.js",
});

if (prod) {
  await context.rebuild();
  process.exit(0);
} else {
  await context.watch();
}

Step 6: Create the Manifest

Create manifest.json:

{
  "id": "word-counter-badge",
  "name": "Word Counter Badge",
  "version": "1.0.0",
  "minAppVersion": "0.15.0",
  "description": "Displays word and character count in the status bar",
  "author": "Your Name",
  "authorUrl": "https://yourwebsite.com",
  "isDesktopOnly": false
}

Building the Plugin

Now for the interesting part! Create main.ts:

import { Plugin, MarkdownView } from "obsidian";

export default class WordCounterPlugin extends Plugin {
  private statusBarItem: HTMLElement;

  async onload() {
    console.log("Loading Word Counter Badge plugin");

    // Create status bar item
    this.statusBarItem = this.addStatusBarItem();
    this.statusBarItem.setText("Words: 0 | Chars: 0");

    // Update counter when active note changes
    this.registerEvent(
      this.app.workspace.on("active-leaf-change", () => {
        this.updateWordCount();
      }),
    );

    // Update counter when file content changes
    this.registerEvent(
      this.app.workspace.on("editor-change", () => {
        this.updateWordCount();
      }),
    );

    // Initial update
    this.updateWordCount();
  }

  onunload() {
    console.log("Unloading Word Counter Badge plugin");
  }

  private updateWordCount() {
    const view = this.app.workspace.getActiveViewOfType(MarkdownView);

    if (!view) {
      this.statusBarItem.setText("Words: 0 | Chars: 0");
      return;
    }

    const content = view.editor.getValue();
    const stats = this.calculateStats(content);

    this.statusBarItem.setText(
      `Words: ${stats.words} | Chars: ${stats.characters}`,
    );
  }

  private calculateStats(text: string): { words: number; characters: number } {
    // Remove markdown syntax for more accurate counting
    const cleanText = text
      // Remove code blocks
      .replace(/```[\s\S]*?```/g, "")
      // Remove inline code
      .replace(/`[^`]*`/g, "")
      // Remove links but keep the text
      .replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
      // Remove images
      .replace(/!\[([^\]]*)\]\([^\)]+\)/g, "")
      // Remove bold/italic markers
      .replace(/[*_]{1,3}/g, "")
      // Remove headers
      .replace(/^#{1,6}\s+/gm, "")
      // Remove blockquotes
      .replace(/^>\s+/gm, "");

    // Count words (split by whitespace and filter empty strings)
    const words = cleanText
      .trim()
      .split(/\s+/)
      .filter((word) => word.length > 0).length;

    // Count characters (excluding spaces)
    const characters = cleanText.replace(/\s/g, "").length;

    return { words, characters };
  }
}

Understanding the Code

Let’s break down the key parts:

The Plugin Class

export default class WordCounterPlugin extends Plugin {

Every Obsidian plugin extends the base Plugin class. This gives you access to the Obsidian API and lifecycle hooks.

Status Bar Item

this.statusBarItem = this.addStatusBarItem();

addStatusBarItem() creates a new element in Obsidian’s status bar. This is where we display our word count.

Event Listeners

this.registerEvent(
  this.app.workspace.on("active-leaf-change", () => {
    this.updateWordCount();
  }),
);

We register two events:

  • active-leaf-change: Fires when you switch to a different note
  • editor-change: Fires when you edit the current note’s content

The registerEvent() method ensures listeners are properly cleaned up when the plugin is disabled.

Word Counting Logic

private calculateStats(text: string): { words: number; characters: number }

This method:

  1. Strips Markdown syntax (code blocks, links, formatting)
  2. Counts words by splitting on whitespace
  3. Counts characters excluding spaces

This provides more accurate counts than simply counting all characters in raw Markdown.

Testing Your Plugin

Step 1: Build the Plugin

npm run dev

This starts esbuild in watch mode, automatically rebuilding when you save changes.

Step 2: Enable the Plugin

  1. Open Obsidian
  2. Go to Settings → Community plugins
  3. Disable “Safe mode” (if enabled)
  4. Click “Browse” and find your plugin in the list
  5. Enable it

Alternatively, since we’re in the plugins folder:

  1. Reload Obsidian (Ctrl/Cmd + R)
  2. Check the console for “Loading Word Counter Badge plugin”

Step 3: Test It

  • Open a note and start typing
  • Watch the status bar update in real-time
  • Switch between notes to verify the count updates
  • Test with different Markdown elements (code blocks, links, etc.)

Debugging Tips

Use the Developer Console

Open it with Ctrl/Cmd + Shift + I. Your console.log() statements will appear here.

Check for Errors

If the plugin doesn’t load, check the console for errors. Common issues:

  • TypeScript compilation errors
  • Missing dependencies
  • Incorrect manifest.json

Hot Reload

After making changes:

  1. Save your files
  2. Reload Obsidian (Ctrl/Cmd + R)
  3. Check if changes are reflected

What’s Next

You now have a working Obsidian plugin! In Part 2, we’ll enhance it with:

  • Settings panel to customize display options
  • Reading time estimation
  • Commands for detailed statistics
  • Distribution and best practices

This foundation demonstrates the core concepts of Obsidian plugin development:

  1. Plugin lifecycle: Understanding onload() and onunload()
  2. Event system: Reacting to workspace changes
  3. UI integration: Working with the status bar
  4. TypeScript skills: Type safety and modern JavaScript features

Continue to Part 2 to take your plugin to the next level!