fix: gitignore docs/superpowers

This commit is contained in:
svemagie
2026-04-14 22:32:38 +02:00
parent 1c551dc785
commit b9e3444bac
2 changed files with 0 additions and 743 deletions
@@ -1,526 +0,0 @@
# i18n (en + de) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add English and German translations to obsidian-micropub by extracting all hardcoded UI strings into locale files and wiring them through a tiny `t()` helper.
**Architecture:** A `src/i18n.ts` module exports a single `t(key, vars?)` function that reads `window.moment.locale()` (Obsidian's own locale) at call time, looks up the key in the matching locale map (falling back to `en`), and does `{var}` substitution. Locale maps live in `src/lang/en.ts` and `src/lang/de.ts`. No external dependencies, no build changes.
**Tech Stack:** TypeScript, Obsidian API (`window.moment.locale()`), esbuild (existing)
---
## File Map
| Action | Path | Responsibility |
|--------|------|---------------|
| Create | `src/lang/en.ts` | English string map (source of truth) |
| Create | `src/lang/de.ts` | German string map |
| Create | `src/i18n.ts` | `t()` helper — locale detection + lookup + interpolation |
| Modify | `src/main.ts` | Replace hardcoded strings with `t()` calls |
| Modify | `src/SettingsTab.ts` | Replace hardcoded strings with `t()` calls |
| Modify | `src/SyndicationDialog.ts` | Replace hardcoded strings with `t()` calls |
| Modify | `src/IndieAuth.ts` | Replace one hardcoded error string with `t()` call |
| Modify | `README.md` | Note i18n support under Configuration |
---
## Task 1: English locale file
**Files:**
- Create: `src/lang/en.ts`
- [ ] **Step 1: Create `src/lang/en.ts` with all UI strings**
```typescript
// src/lang/en.ts
export const en: Record<string, string> = {
// Commands & ribbon
cmdPublish: "Publish to Micropub",
cmdUpdate: "Update existing Micropub post",
// Notices — main.ts
noticeOpenNote: "Open a Markdown note to publish.",
noticeNoEndpoint: "⚠️ Micropub endpoint not configured. Open plugin settings to add it.",
noticeNoToken: "⚠️ Access token not configured. Open plugin settings to add it.",
noticePublishing: "Publishing…",
noticePublished: "✅ Published!",
noticePublishFailed: "❌ Publish failed: {error}",
noticeError: "❌ Error: {error}",
noticeNoSyndTargets: "⚠️ Could not fetch syndication targets. Publishing without dialog.",
// Settings headings
settingsTitle: "Micropub Publisher",
settingsAccount: "Account",
settingsEndpoints: "Endpoints",
settingsEndpointsHint: "These are filled automatically when you sign in. Only edit them manually if your server uses non-standard paths.",
settingsPublishBehaviour:"Publish Behaviour",
settingsDigitalGarden: "Digital Garden",
// Settings — endpoints
settingMicropubEndpoint: "Micropub endpoint",
settingMicropubEndpointDesc: "e.g. https://example.com/micropub", // intentional: replaces personal domain in source
settingMediaEndpoint: "Media endpoint",
settingMediaEndpointDesc:"For image uploads. Auto-discovered if blank.",
// Settings — publish behaviour
settingVisibility: "Default visibility",
settingVisibilityDesc: "Applies when the note has no explicit visibility property.",
visibilityPublic: "Public",
visibilityUnlisted: "Unlisted",
visibilityPrivate: "Private",
settingWriteUrl: "Write URL back to note",
settingWriteUrlDesc: "After publishing, store the post URL as `mp-url` in frontmatter. Subsequent publishes will update the existing post instead of creating a new one.",
settingSyndDialog: "Syndication dialog",
settingSyndDialogDesc: "When to show the cross-posting dialog before publishing. 'When needed' shows it only if the note has no mp-syndicate-to frontmatter.",
syndDialogWhenNeeded: "When needed",
syndDialogAlways: "Always",
syndDialogNever: "Never",
settingSyndDefaults: "Default syndication targets",
settingSyndDefaultsNone: "None configured. Targets checked by default in the publish dialog.",
btnClearDefaults: "Clear defaults",
// Settings — digital garden
settingGardenTags: "Map #garden/* tags to gardenStage",
settingGardenTagsDesc: "Obsidian tags like #garden/plant become a `garden-stage: plant` Micropub property. The blog renders these as growth stage badges at /garden/.",
settingGardenStages: "Stages: plant 🌱 · cultivate 🌿 · question ❓ · repot 🪴 · revitalize ✨ · revisit 🔄",
// Settings — sign-in / sign-out
settingSiteUrl: "Site URL",
settingSiteUrlDesc: "Your site's home page. Clicking Sign in opens your blog's login page in the browser — the same flow iA Writer uses.",
settingSiteUrlPlaceholder: "https://example.com", // intentional: replaces personal domain in source
btnSignIn: "Sign in",
btnOpeningBrowser: "Opening browser…",
noticeEnterSiteUrl: "Enter your site URL first.",
noticeSignedInAs: "✅ Signed in as {me}",
noticeSignInFailed: "Sign-in failed: {error}",
lblSignedIn: "Signed in",
btnSignOut: "Sign out",
manualTokenSummary: "Or paste a token manually",
settingAccessToken: "Access token",
settingAccessTokenDesc: "Bearer token from your Indiekit admin panel.",
btnVerify: "Verify",
noticeSetEndpointFirst: "Set the Micropub endpoint and token first.",
noticeTokenValid: "✅ Token is valid!",
noticeTokenCheckFailed: "Token check failed: {error}",
// Syndication dialog
syndDialogTitle: "Syndication targets",
syndDialogSubtitle: "Choose where to cross-post this note.",
btnCancel: "Cancel",
btnPublish: "Publish",
// IndieAuth
errSignInTimeout: "Sign-in timed out (5 min). Please try again.",
};
```
- [ ] **Step 2: Build to confirm no TypeScript errors**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
```
Expected: exits 0.
- [ ] **Step 3: Commit**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
git add src/lang/en.ts
git commit -m "feat(i18n): add English locale file with all UI strings"
```
---
## Task 2: German locale file
**Files:**
- Create: `src/lang/de.ts`
- [ ] **Step 1: Create `src/lang/de.ts`**
```typescript
// src/lang/de.ts
export const de: Record<string, string> = {
// Commands & ribbon
cmdPublish: "An Micropub veröffentlichen",
cmdUpdate: "Bestehenden Micropub-Beitrag aktualisieren",
// Notices — main.ts
noticeOpenNote: "Öffne eine Markdown-Notiz zum Veröffentlichen.",
noticeNoEndpoint: "⚠️ Micropub-Endpunkt nicht konfiguriert. Bitte in den Plugin-Einstellungen eintragen.",
noticeNoToken: "⚠️ Zugriffstoken nicht konfiguriert. Bitte in den Plugin-Einstellungen eintragen.",
noticePublishing: "Wird veröffentlicht…",
noticePublished: "✅ Veröffentlicht!",
noticePublishFailed: "❌ Veröffentlichung fehlgeschlagen: {error}",
noticeError: "❌ Fehler: {error}",
noticeNoSyndTargets: "⚠️ Syndizierungsziele konnten nicht abgerufen werden. Veröffentlichung ohne Dialog.",
// Settings headings
settingsTitle: "Micropub Publisher",
settingsAccount: "Konto",
settingsEndpoints: "Endpunkte",
settingsEndpointsHint: "Diese werden beim Anmelden automatisch ausgefüllt. Nur manuell bearbeiten, wenn der Server nicht standardmäßige Pfade verwendet.",
settingsPublishBehaviour:"Veröffentlichungsverhalten",
settingsDigitalGarden: "Digitaler Garten",
// Settings — endpoints
settingMicropubEndpoint: "Micropub-Endpunkt",
settingMicropubEndpointDesc: "z. B. https://example.com/micropub",
settingMediaEndpoint: "Medien-Endpunkt",
settingMediaEndpointDesc:"Für Bild-Uploads. Wird automatisch ermittelt, wenn leer.",
// Settings — publish behaviour
settingVisibility: "Standard-Sichtbarkeit",
settingVisibilityDesc: "Gilt, wenn die Notiz keine explizite Sichtbarkeits-Eigenschaft hat.",
visibilityPublic: "Öffentlich",
visibilityUnlisted: "Nicht gelistet",
visibilityPrivate: "Privat",
settingWriteUrl: "URL zurück in Notiz schreiben",
settingWriteUrlDesc: "Nach der Veröffentlichung wird die Beitrags-URL als `mp-url` im Frontmatter gespeichert. Spätere Veröffentlichungen aktualisieren den Beitrag statt einen neuen zu erstellen.",
settingSyndDialog: "Syndizierungsdialog",
settingSyndDialogDesc: "Wann der Dialog zum Querverweis vor der Veröffentlichung angezeigt wird. 'Bei Bedarf' zeigt ihn nur, wenn kein mp-syndicate-to im Frontmatter vorhanden ist.",
syndDialogWhenNeeded: "Bei Bedarf",
syndDialogAlways: "Immer",
syndDialogNever: "Nie",
settingSyndDefaults: "Standard-Syndizierungsziele",
settingSyndDefaultsNone: "Keine konfiguriert. Im Veröffentlichungsdialog standardmäßig aktivierte Ziele.",
btnClearDefaults: "Standards löschen",
// Settings — digital garden
settingGardenTags: "#garden/*-Tags zu gardenStage zuordnen",
settingGardenTagsDesc: "Obsidian-Tags wie #garden/plant werden zur Micropub-Eigenschaft `garden-stage: plant`. Der Blog zeigt diese als Wachstumsstufen-Abzeichen unter /garden/ an.",
settingGardenStages: "Stufen: plant 🌱 · cultivate 🌿 · question ❓ · repot 🪴 · revitalize ✨ · revisit 🔄",
// Settings — sign-in / sign-out
settingSiteUrl: "Website-URL",
settingSiteUrlDesc: "Startseite deiner Website. Klick auf Anmelden öffnet die Login-Seite deines Blogs im Browser.",
settingSiteUrlPlaceholder: "https://example.com",
btnSignIn: "Anmelden",
btnOpeningBrowser: "Browser wird geöffnet…",
noticeEnterSiteUrl: "Bitte zuerst die Website-URL eingeben.",
noticeSignedInAs: "✅ Angemeldet als {me}",
noticeSignInFailed: "Anmeldung fehlgeschlagen: {error}",
lblSignedIn: "Angemeldet",
btnSignOut: "Abmelden",
manualTokenSummary: "Oder Token manuell einfügen",
settingAccessToken: "Zugriffstoken",
settingAccessTokenDesc: "Bearer-Token aus deinem Indiekit-Adminbereich.",
btnVerify: "Prüfen",
noticeSetEndpointFirst: "Bitte zuerst Micropub-Endpunkt und Token eingeben.",
noticeTokenValid: "✅ Token ist gültig!",
noticeTokenCheckFailed: "Token-Prüfung fehlgeschlagen: {error}",
// Syndication dialog
syndDialogTitle: "Syndizierungsziele",
syndDialogSubtitle: "Wo soll diese Notiz gleichzeitig veröffentlicht werden?",
btnCancel: "Abbrechen",
btnPublish: "Veröffentlichen",
// IndieAuth
errSignInTimeout: "Anmeldung abgelaufen (5 Min.). Bitte erneut versuchen.",
};
```
- [ ] **Step 2: Build**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
```
Expected: exits 0.
- [ ] **Step 3: Commit**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
git add src/lang/de.ts
git commit -m "feat(i18n): add German locale file"
```
---
## Task 3: i18n helper
**Files:**
- Create: `src/i18n.ts`
- [ ] **Step 1: Create `src/i18n.ts`**
```typescript
// src/i18n.ts
import { en } from "./lang/en";
import { de } from "./lang/de";
const locales: Record<string, Record<string, string>> = { en, de };
/**
* Returns the translated string for `key` in the active Obsidian locale.
* Falls back to English if the locale or key is missing.
*
* Supports `{var}` interpolation:
* t("noticePublishFailed", { error: "500" })
* → "❌ Publish failed: 500"
*/
export function t(key: string, vars?: Record<string, string>): string {
const lang = (window.moment?.locale() ?? "en").split("-")[0];
const map = locales[lang] ?? locales["en"];
let str = map[key] ?? locales["en"][key] ?? key;
if (vars) {
for (const [k, v] of Object.entries(vars)) {
str = str.split(`{${k}}`).join(v); // replaceAll not available in ES2018
}
}
return str;
}
```
- [ ] **Step 2: Build**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
```
Expected: exits 0.
- [ ] **Step 3: Commit**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
git add src/i18n.ts
git commit -m "feat(i18n): add t() helper with locale detection and interpolation"
```
---
## Task 4: Wire `t()` into `main.ts`
**Files:**
- Modify: `src/main.ts`
- [ ] **Step 1: Add import and replace all strings**
At the top of `main.ts`, add:
```typescript
import { t } from "./i18n";
```
Then replace each hardcoded string:
| Old | New |
|-----|-----|
| `"Publish to Micropub"` (command name, line 36) | `t("cmdPublish")` |
| `"Update existing Micropub post"` (line 49) | `t("cmdUpdate")` |
| `"Publish to Micropub"` (ribbon tooltip, line 75) | `t("cmdPublish")` |
| `"Open a Markdown note to publish."` (line 78) | `t("noticeOpenNote")` |
| `"⚠️ Micropub endpoint not configured…"` (line 93) | `t("noticeNoEndpoint")` |
| `"⚠️ Access token not configured…"` (line 100) | `t("noticeNoToken")` |
| `"Publishing…"` (line 114) | `t("noticePublishing")` |
| `` `✅ Published!${urlDisplay}` `` (line 126) | `` `${t("noticePublished")}${urlDisplay}` `` |
| `` `❌ Publish failed: ${result.error}` `` (line 128) | `t("noticePublishFailed", { error: result.error ?? "" })` |
| `` `❌ Error: ${msg}` `` (line 134) | `t("noticeError", { error: msg })` |
| `"⚠️ Could not fetch syndication targets…"` (line 167) | `t("noticeNoSyndTargets")` |
- [ ] **Step 2: Build**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
```
Expected: exits 0.
- [ ] **Step 3: Commit**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
git add src/main.ts
git commit -m "feat(i18n): wire t() into main.ts"
```
---
## Task 5: Wire `t()` into `SettingsTab.ts`
**Files:**
- Modify: `src/SettingsTab.ts`
- [ ] **Step 1: Add import**
```typescript
import { t } from "./i18n";
```
- [ ] **Step 2: Replace strings in `display()`**
| Old | New |
|-----|-----|
| `"Micropub Publisher"` (h2) | `t("settingsTitle")` |
| `"Account"` (h3) | `t("settingsAccount")` |
| `"Endpoints"` (h3) | `t("settingsEndpoints")` |
| `"These are filled automatically…"` (p) | `t("settingsEndpointsHint")` |
| `"Micropub endpoint"` (setName) | `t("settingMicropubEndpoint")` |
| `"e.g. https://blog.giersig.eu/micropub"` (setDesc) | `t("settingMicropubEndpointDesc")` |
| `"Media endpoint"` (setName) | `t("settingMediaEndpoint")` |
| `"For image uploads…"` (setDesc) | `t("settingMediaEndpointDesc")` |
| `"Publish Behaviour"` (h3) | `t("settingsPublishBehaviour")` |
| `"Default visibility"` (setName) | `t("settingVisibility")` |
| `"Applies when the note…"` (setDesc) | `t("settingVisibilityDesc")` |
| `"Public"` | `t("visibilityPublic")` |
| `"Unlisted"` | `t("visibilityUnlisted")` |
| `"Private"` | `t("visibilityPrivate")` |
| `"Write URL back to note"` (setName) | `t("settingWriteUrl")` |
| `"After publishing, store…"` (setDesc) | `t("settingWriteUrlDesc")` |
| `"Syndication dialog"` (setName) | `t("settingSyndDialog")` |
| `"When to show the cross-posting…"` (setDesc) | `t("settingSyndDialogDesc")` |
| `"When needed"` | `t("syndDialogWhenNeeded")` |
| `"Always"` | `t("syndDialogAlways")` |
| `"Never"` | `t("syndDialogNever")` |
| `"Default syndication targets"` (setName) | `t("settingSyndDefaults")` |
| `"None configured…"` (false branch of setDesc — when `defaults.length === 0`) | `t("settingSyndDefaultsNone")` |
| `"Clear defaults"` (btn) | `t("btnClearDefaults")` |
| `"Digital Garden"` (h3) | `t("settingsDigitalGarden")` |
| `"Map #garden/* tags…"` (setName) | `t("settingGardenTags")` |
| `"Obsidian tags like…"` (setDesc) | `t("settingGardenTagsDesc")` |
| `"Stages: plant 🌱…"` (p) | `t("settingGardenStages")` |
- [ ] **Step 3: Replace strings in `renderSignedOut()`**
| Old | New |
|-----|-----|
| `"Site URL"` (setName) | `t("settingSiteUrl")` |
| `"Your site's home page…"` (setDesc) | `t("settingSiteUrlDesc")` |
| `"https://blog.giersig.eu"` (placeholder) | `t("settingSiteUrlPlaceholder")` |
| `"Sign in"` (btn) | `t("btnSignIn")` |
| `"Enter your site URL first."` (Notice) | `t("noticeEnterSiteUrl")` |
| `"Opening browser…"` (btn) | `t("btnOpeningBrowser")` |
| `` `✅ Signed in as ${result.me}` `` | `t("noticeSignedInAs", { me: result.me })` |
| `` `Sign-in failed: ${String(err)}` `` | `t("noticeSignInFailed", { error: String(err) })` |
| `"Sign in"` (catch btn reset) | `t("btnSignIn")` |
| `"Or paste a token manually"` (summary) | `t("manualTokenSummary")` |
| `"Access token"` (setName) | `t("settingAccessToken")` |
| `"Bearer token from your Indiekit admin panel."` (setDesc) | `t("settingAccessTokenDesc")` |
| `"Verify"` (btn) | `t("btnVerify")` |
| `"Set the Micropub endpoint and token first."` (Notice) | `t("noticeSetEndpointFirst")` |
| `"✅ Token is valid!"` (Notice) | `t("noticeTokenValid")` |
| `` `Token check failed: ${String(err)}` `` | `t("noticeTokenCheckFailed", { error: String(err) })` |
- [ ] **Step 4: Replace strings in `renderSignedIn()`**
| Old | New |
|-----|-----|
| `"Signed in"` (label) | `t("lblSignedIn")` |
| `"Site URL"` (setName) | `t("settingSiteUrl")` |
| `"Sign out"` (btn) | `t("btnSignOut")` |
- [ ] **Step 5: Build**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
```
Expected: exits 0.
- [ ] **Step 6: Commit**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
git add src/SettingsTab.ts
git commit -m "feat(i18n): wire t() into SettingsTab.ts"
```
---
## Task 6: Wire `t()` into `SyndicationDialog.ts` and `IndieAuth.ts`
**Files:**
- Modify: `src/SyndicationDialog.ts`
- Modify: `src/IndieAuth.ts`
- [ ] **Step 1: Update `SyndicationDialog.ts`**
Add import:
```typescript
import { t } from "./i18n";
```
Replace:
| Old | New |
|-----|-----|
| `"Syndication targets"` (h2) | `t("syndDialogTitle")` |
| `"Choose where to cross-post this note."` (p) | `t("syndDialogSubtitle")` |
| `"Cancel"` (btn) | `t("btnCancel")` |
| `"Publish"` (btn) | `t("btnPublish")` |
- [ ] **Step 2: Update `IndieAuth.ts`**
Add import at top:
```typescript
import { t } from "./i18n";
```
Replace:
| Old | New |
|-----|-----|
| `"Sign-in timed out (5 min). Please try again."` | `t("errSignInTimeout")` |
- [ ] **Step 3: Build**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
```
Expected: exits 0.
- [ ] **Step 4: Commit**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
git add src/SyndicationDialog.ts src/IndieAuth.ts
git commit -m "feat(i18n): wire t() into SyndicationDialog and IndieAuth"
```
---
## Task 7: README update
**Files:**
- Modify: `README.md`
- [ ] **Step 1: Add i18n note to Configuration section**
After the Settings reference table, add:
```markdown
### Language
The plugin follows Obsidian's display language. Set it in **Settings → About → Language**. Currently supported: English (`en`), German (`de`).
```
- [ ] **Step 2: Add `SyndicationDialog.ts` already done in prior session (verify)**
Architecture table should already show `SyndicationDialog.ts` from the prior README update.
- [ ] **Step 3: Commit**
```bash
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
git add README.md
git commit -m "docs: note i18n support (en/de) in README"
```
---
## Manual verification checklist
After all tasks, load the plugin in Obsidian and verify:
- [ ] With Obsidian in English: all UI labels, notices, and dialog text appear in English
- [ ] With Obsidian in German (`de`): all UI labels, notices, and dialog text appear in German
- [ ] Publishing a note shows the correct locale notice (`Publishing…` / `Wird veröffentlicht…`)
- [ ] The syndication dialog title and buttons are translated
- [ ] Sign-in flow button text and error notices are translated
- [ ] Switching locale and restarting Obsidian picks up the new language
@@ -1,217 +0,0 @@
# Syndication Dialog Design
**Date:** 2026-03-30
**Status:** Approved
**Scope:** obsidian-micropub plugin feature
---
## 1. Overview
Add a dialog that appears when publishing to Micropub, allowing users to select which syndication targets (e.g., Twitter, Mastodon) to cross-post to. The dialog integrates with the existing `?q=config` Micropub endpoint to fetch available targets.
---
## 2. User Flow
1. User clicks "Publish to Micropub"
2. Plugin fetches `?q=config` to get available syndication targets
3. Plugin checks frontmatter for `mp-syndicate-to`:
- **Has values** → use those, skip dialog, publish
- **Empty array `[]`** → force dialog
- **Absent** → show dialog with defaults pre-checked
4. Dialog displays checkboxes for each target from server
5. User confirms → publish with selected targets
6. Successful publish writes `mp-syndicate-to` to frontmatter
---
## 3. Configuration
### New Settings
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `showSyndicationDialog` | enum | `"when-needed"` | When to show the dialog |
| `defaultSyndicateTo` | string[] | `[]` | Targets checked by default |
### `showSyndicationDialog` Options
- `"when-needed"` — Show only if `mp-syndicate-to` is absent from frontmatter
- `"always"` — Show every time user publishes
- `"never"` — Use defaults, never show dialog
### Frontmatter Support
Users can bypass the dialog per-note using frontmatter:
```yaml
---
# Skip dialog, auto-syndicate to these targets
mp-syndicate-to: [twitter, mastodon]
# Force dialog even with defaults set
mp-syndicate-to: []
---
```
---
## 4. Components
### 4.1 SyndicationDialog (New)
**Location:** `src/SyndicationDialog.ts`
**Responsibilities:**
- Render modal with checkbox list of targets
- Pre-check targets from `defaultSyndicateTo` setting
- Handle OK/Cancel actions
- Return selected target UIDs via promise
**Interface:**
```typescript
export class SyndicationDialog extends Modal {
constructor(
app: App,
targets: SyndicationTarget[],
defaultSelected: string[]
);
/**
* Opens the dialog and waits for user selection.
* @returns Selected target UIDs, or null if cancelled.
*/
async awaitSelection(): Promise<string[] | null>;
}
```
### 4.2 Publisher (Modified)
**Changes:**
- Accept optional `syndicateToOverride?: string[]` parameter
- Merge override with frontmatter values (override wins)
- Write `mp-syndicate-to` to frontmatter on successful publish
### 4.3 SettingsTab (Modified)
**Changes:**
- Add dropdown for `showSyndicationDialog` behavior
- Display currently configured default targets (read-only list)
- Add button to clear defaults
### 4.4 main.ts (Modified)
**Changes:**
- Before calling `publishActiveNote`:
1. Fetch `?q=config` for syndication targets
2. Check frontmatter for `mp-syndicate-to`
3. Decide whether to show dialog based on setting + frontmatter
4. If showing dialog, wait for user selection
5. Call `publisher.publish()` with selected targets
---
## 5. Data Flow
```
User clicks "Publish"
Fetch ?q=config ──► Check frontmatter mp-syndicate-to
│ │
│ ┌─────────────┼─────────────┐
│ │ │ │
│ Has values Absent Empty []
│ (skip dialog) (show dialog) (show dialog)
│ │ │ │
│ └─────────────┴─────────────┘
│ │
▼ ▼
SyndicationDialog (if needed)
Publisher.publish(selectedTargets?)
Write mp-syndicate-to to frontmatter
```
---
## 6. Error Handling
| Scenario | Behavior |
|----------|----------|
| `?q=config` fails | Warn user, offer to publish without syndication or cancel |
| Dialog canceled | Abort publish, no changes |
| Micropub POST fails | Don't write `mp-syndicate-to` to frontmatter |
| No targets returned from server | Skip dialog, publish normally (backward compatible) |
---
## 7. UI/UX Details
### Dialog Layout
```
┌─────────────────────────────────────────┐
│ Publish to Syndication Targets │
├─────────────────────────────────────────┤
│ │
│ [✓] Twitter (@username) │
│ [✓] Mastodon (@user@instance) │
│ [ ] LinkedIn │
│ │
├─────────────────────────────────────────┤
│ [Cancel] [Publish] │
└─────────────────────────────────────────┘
```
### Settings UI Addition
```
Publish Behaviour
├── Default visibility: [public ▼]
├── Write URL back to note: [✓]
├── Syndication dialog: [when-needed ▼]
│ └── when-needed: Show only if no mp-syndicate-to
├── Default syndication targets:
│ └── twitter, mastodon [Clear defaults]
└── ...
```
---
## 8. Edge Cases
1. **User has no syndication targets configured on server** — Skip dialog, publish normally
2. **User cancels dialog** — Abort publish entirely, no state changes
3. **Micropub server returns targets but some are invalid** — Show all, let server reject invalid ones
4. **User changes targets in settings after publishing** — Affects future publishes only, doesn't retroactively change existing `mp-syndicate-to` frontmatter
---
## 9. Backward Compatibility
- Default `showSyndicationDialog: "when-needed"` means existing behavior unchanged for notes without frontmatter
- Existing `mp-syndicate-to` frontmatter values continue to work
- Plugin remains compatible with servers that don't return syndication targets
---
## 10. Testing Considerations
- Unit test: `SyndicationDialog` renders checkboxes correctly
- Unit test: Frontmatter parsing handles `mp-syndicate-to` array
- Unit test: Setting `"never"` skips dialog
- Integration test: Full flow from click to publish with targets
- Edge case: Server returns empty targets array
- Edge case: User cancels dialog
---
## Approval
**Approved by:** @svemagie
**Date:** 2026-03-30