Case Study
Building the POSSE Publisher
Writers who use Obsidian as their primary tool face a broken publishing workflow: the note lives in the vault, but getting it onto a personal site, let alone syndicating it to Dev.to, Mastodon, and Bluesky; requires manual copying, reformatting, and cross-posting. POSSE Publisher solves this with a single Obsidian plugin that publishes to a canonical personal site first, then syndicates copies to every configured platform, writing the syndicated URLs back into the note's own frontmatter. The plugin is open source, available on the Obsidian community marketplace, and has shipped three major versions.

Background & Context
Obsidian has become the writing environment of choice for a growing number of developers, bloggers, and knowledge workers who value local-first, Markdown-based notes. But Obsidian is a writing tool, not a publishing platform — and the gap between "finished note" and "published post" typically involves at least two manual steps: pushing content to a CMS and separately posting to distribution platforms.
The IndieWeb community has long advocated for a solution: POSSE (Publish on your Own Site, Syndicate Elsewhere). The idea is simple, publish the canonical version on your own domain, then syndicate copies to the silos (Dev.to, Mastodon, Bluesky, LinkedIn, Reddit), with every copy linking back to the original. This keeps the domain authoritative for search engines while still reaching audiences where they are.
No existing Obsidian plugin implemented POSSE end-to-end. POSSE Publisher was built to close that gap.
Problem Statement
The Question: How do you implement the POSSE publishing pattern without leaving the Obsidian writing environment?
The Problem: Content creators using Obsidian must break out of their writing context every time they publish, copy-pasting to a CMS, then manually cross-posting to each distribution platform, with no record inside the note of where it's been syndicated. Content published directly to silos (Medium, Dev.to) becomes owned by those platforms, indexed on their domain, and disconnected from the canonical version on the writer's site.
The Challenge: Building a plugin that handles multiple destination types (custom REST API, Dev.to, Mastodon, Bluesky) behind a single interface required writing type-specific payload builders and credential flows for each platform, without letting the complexity leak into the user-facing settings UI. Supporting Obsidian's own markdown syntax (wiki-links, embeds, Dataview blocks) meant the plugin also needed to preprocess content before it ever hit an external API.
Methodology / Approach
The plugin was built in TypeScript using the Obsidian plugin API and bundled with esbuild for a zero-dependency runtime footprint. Each destination type, custom-api, devto, mastodon, bluesky, has its own credential configuration and payload adapter, but all destinations share a common buildPayload() method that handles frontmatter extraction, slug generation, and Obsidian syntax stripping.
A shared markdownToHtml() converter handles platforms that require HTML bodies; markdownToPlainText() strips all Markdown syntax for character-limited platforms like Threads and LinkedIn.
Syndication tracking was a design-first decision: after a successful publish, the plugin writes the syndicated URL back into the note's frontmatter under a syndication: field. This makes the note the permanent record of where its content lives across the web, eliminating the need for any external tracker.
The settings UI was built to scale, adding a new destination type requires only a new credential block in the settings tab and a new adapter in the publish flow. The destination picker modal handles the "multiple destinations" case, keeping single-destination users on a fast path.
Analysis and Findings
Four design constraints emerged early:
Canonical URL is the load-bearing concept. Without a stable canonical URL, syndicated copies have nothing to link back to. Every payload now includes a canonicalUrl field, either from explicit frontmatter or auto-generated from the configured base URL and the post slug. Getting this right required fixing a bug where the field was ignored even when explicitly set.
Frontmatter must be the single source of truth. Users shouldn't need a separate dashboard to know where a post has been published. Writing syndication URLs back into the note's own frontmatter was the right architectural call — it keeps the note self-describing and makes the data portable.
Obsidian's markdown is not standard markdown. Wiki-links ([[Page Name]]), embedded notes (![[note]]), Dataview blocks, and Obsidian-style comments (%%text%%) all need to be stripped or converted before content reaches any external API. This preprocessing step is configurable but on by default.
Auto-publish on save is a power feature that must be safe by default. Triggering a live publish on every save would cause rapid-fire API requests while a note is being edited. The implementation debounces by 3 seconds and restricts auto-publish to notes with status: published in frontmatter — drafts are never auto-published regardless of settings.
Solutions and Implementation
Version 1.0 - Private Publishing Tool
The plugin began as a single-purpose tool: push a note from Obsidian to a personal site's /api/publish endpoint. Core features included multi-site destination support with a picker modal, three publish commands (publish, publish as draft, publish live), full YAML frontmatter parsing, auto-slug generation from note title, upsert-by-slug behavior on the server, and Obsidian markdown preprocessing.
Conclusion and Lessons Learned
POSSE Publisher shows that a writing workflow can be made publishing-native without adding friction. The key architectural decision was treating frontmatter as the permanent record, not just input metadata, but the output log of every successful syndication. That choice made syndication tracking feel natural rather than bolted on, and it kept the plugin's data model simple: the note knows where it lives.
The evolution from a private single-site tool to a public multi-platform plugin also illustrated a useful heuristic: solve your own problem completely before generalizing. Version 1.0 was built entirely for personal use. The POSSE rebrand happened after the core behavior was stable and the value was clear, not as upfront product design.
The biggest remaining challenge is completing the syndication adapters for Medium, Reddit, Threads, LinkedIn, and Ecency. The settings UI is already wired; the remaining work is writing and testing each platform's API integration.
Project README
POSSE Publisher — Obsidian Plugin
Publish on your Own Site, Syndicate Elsewhere.
POSSE Publisher brings the IndieWeb POSSE philosophy to Obsidian. Write once in your vault, publish to your canonical site first, then syndicate copies to platforms like Dev.to, Mastodon, Bluesky, Medium, Reddit, Threads, LinkedIn, and Ecency — with every syndicated copy linking back to your original.
Your content. Your domain. Your canonical URL.
Quick start
- Open Settings > POSSE Publisher.
- Enter your canonical base URL for your site.
- Click Add destination and configure one supported destination.
- Open a note and add simple frontmatter:
---
title: My first post
status: draft
---
- Run POSSE publish from the command palette or use the ribbon icon.
Start with Custom API, Dev.to, Mastodon, or Bluesky for the fastest setup.
What is POSSE?
POSSE is a publishing strategy from the IndieWeb community:
- Publish the original on your own site (blog, portfolio, etc.)
- Syndicate copies to silos (Dev.to, Mastodon, Bluesky, etc.)
- Every copy links back to the canonical original you own
This means your domain holds the canonical version, search engines index your site, and you keep full ownership — while still reaching audiences on the platforms they use.
Installation
From Community Plugins (Recommended)
- Open Settings → Community plugins → Browse
- Search for "POSSE Publisher"
- Click Install, then Enable
Manual Install
- Download
main.js,manifest.json, andstyles.cssfrom the latest release - Create a folder at
<vault>/.obsidian/plugins/posse-publisher/ - Copy the downloaded files into that folder
- Restart Obsidian and enable the plugin under Settings → Community Plugins
Build from Source
git clone https://github.com/TheOfficialDM/posse-publisher.git
cd posse-publisher
npm install
npm run build
Copy main.js, manifest.json, and styles.css into your vault's plugins folder.
Setup
- Open Settings > POSSE Publisher.
- Enter your canonical base URL. This is your site's root URL.
- Click Add destination and start with one supported platform.
- Paste the API key or token for that destination.
- Open a note, then run POSSE publish.
Destinations
Add as many destinations as you need. Each destination has a type that controls how content is formatted and delivered.
| Type | Platform | Auth | Status |
|---|---|---|---|
custom-api |
Your own site's /api/publish endpoint |
API key (x-publish-key header) |
Live |
devto |
Dev.to | API key | Live |
mastodon |
Mastodon (any instance) | Access token | Live |
bluesky |
Bluesky (bsky.app) | App password | Live |
medium |
Medium | Integration token | Coming soon |
reddit |
OAuth2 (client ID + secret + refresh token) | Coming soon | |
threads |
Threads | Meta access token | Coming soon |
linkedin |
OAuth2 bearer token | Coming soon | |
ecency |
Ecency (Hive blockchain) | Hive posting key | Coming soon |
Note: Start with Custom API, Dev.to, Mastodon, or Bluesky. The other destinations can stay unconfigured until support is live.
- 1 destination — commands publish directly
- 2+ destinations — a picker modal lets you choose the target
- POSSE to All command — syndicate to every destination at once
Commands
| Command | Behaviour |
|---|---|
| POSSE Publish | Publish using frontmatter status or the default from settings |
| POSSE Publish as Draft | Forces status: draft |
| POSSE Publish Live | Forces status: published |
| POSSE to All | Syndicates to every configured destination |
| POSSE Insert Frontmatter Template | Inserts a YAML template with all supported fields |
A ribbon icon is also available for one-click publishing.
Frontmatter
Use this minimum frontmatter to get started:
---
title: My post title
status: draft
---
Add more fields only when you need them.
Full example:
---
title: My Post Title
slug: my-post-title
excerpt: A short summary
type: blog
status: draft
tags: [javascript, web]
pillar: Technology
coverImage: https://example.com/image.jpg
featured: false
metaTitle: SEO Title Override
metaDescription: SEO description for search results
ogImage: https://example.com/og.jpg
videoUrl: https://youtube.com/watch?v=example
canonicalUrl: https://yoursite.com/blog/my-post-title
syndication:
- url: https://dev.to/you/my-post-title
name: Dev.to
- url: https://mastodon.social/@you/status/123
name: Mastodon
---
| Field | Required | Default |
|---|---|---|
title |
No | File name |
slug |
No | Auto-generated from title |
status |
No | Plugin default setting |
type |
No | blog |
excerpt |
No | Empty |
tags |
No | [] |
pillar |
No | Empty |
coverImage |
No | Empty |
featured |
No | false |
metaTitle |
No | Empty |
metaDescription |
No | Empty |
ogImage |
No | Empty |
videoUrl |
No | Empty |
canonicalUrl |
No | Auto-generated from canonical base URL + slug; set explicitly in frontmatter to override |
syndication |
Auto-set | Written back (and kept current) after each successful publish |
Syndication Tracking
After a successful publish, POSSE Publisher writes the syndicated URL back into your note's frontmatter:
syndication:
- url: https://dev.to/you/my-post
name: Dev.to
This creates a permanent record of where your content has been syndicated — right in the note itself.
Obsidian Syntax Handling
By default, the plugin pre-processes content before publishing:
[[wiki-links]]→ converted to plain text[[target|alias]]→ converted to the alias text![[embeds]]→ removed%%comments%%→ removed```dataview/```dataviewjsblocks → removed
Toggle off in settings if your destination handles Obsidian markdown natively.
Custom API Contract
For custom-api destinations, your /api/publish endpoint should:
- Accept
POSTrequests with a JSON body - Authenticate via the
x-publish-keyheader - Receive a
canonicalUrlfield pointing to the original post on your site - Return
2xxon success, with optional{ "upserted": true }in the response body - Return
4xx/5xxon failure, with optional{ "error": "message" }in the response body
Generate a secure API key with:
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) })
Upsert Behaviour
Publishing a note with the same slug as an existing entry will update that entry instead of creating a duplicate (for custom API destinations).
Troubleshooting
| Problem | Solution |
|---|---|
| "Open a markdown file first" | Make sure you have a .md file active in the editor |
| Publish fails with 401/403 | Check your API key in settings matches the server's PUBLISH_API_KEY |
| Content appears empty | Ensure you have content below the YAML frontmatter fence |
| Wiki-links appear in published content | Enable "Strip Obsidian Syntax" in settings |
| Connection test fails | Verify the destination URL is correct and the server is running |
| Dev.to returns 422 | Check that title frontmatter is set — Dev.to requires a title |
| Mastodon post not appearing | Verify your access token has write:statuses scope |
Security
API keys and access tokens are stored in Obsidian's plugin data directory and are never logged or exposed in the UI (password fields with autocomplete disabled). Always use https:// endpoints.
IndieWeb
This plugin implements the POSSE pattern from the IndieWeb community.
Learn more: indieweb.org · indieweb.org/POSSE
License
MIT — Devin Marshall
Support
POSSE Publisher is free and open source. If it saves you time, a small contribution helps support continued development.
| ☕ Buy Me a Coffee | buymeacoffee.com/theofficaldm |
| ❤ GitHub Sponsors | github.com/sponsors/TheOfficialDM |
| 🔗 All options | devinmarshall.info/fund |

Project Gallery
This album acts as the gallery for the POSSE Publisher portfolio's gallery.


