mirror of
https://github.com/svemagie/obsidian-micropub.git
synced 2026-05-15 20:08:51 +02:00
feat: use kebab-case AI frontmatter keys (ai-text-level, ai-tools, etc.)
Micropub properties now sent as kebab-case; camelCase still accepted as input for backward compatibility. README updated accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,9 +68,9 @@ Tag any note in Obsidian with a `#garden/*` tag, or set `gardenStage` directly i
|
|||||||
|
|
||||||
| Obsidian tag | Published property | Blog display |
|
| Obsidian tag | Published property | Blog display |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `#garden/plant` | `gardenStage: plant` | 🌱 Seedling |
|
|
||||||
| `#garden/cultivate` | `gardenStage: cultivate` | 🌿 Growing |
|
|
||||||
| `#garden/evergreen` | `gardenStage: evergreen` | 🌳 Evergreen |
|
| `#garden/evergreen` | `gardenStage: evergreen` | 🌳 Evergreen |
|
||||||
|
| `#garden/cultivate` | `gardenStage: cultivate` | 🌿 Growing |
|
||||||
|
| `#garden/plant` | `gardenStage: plant` | 🌱 Seedling |
|
||||||
| `#garden/question` | `gardenStage: question` | ❓ Open Question |
|
| `#garden/question` | `gardenStage: question` | ❓ Open Question |
|
||||||
| `#garden/repot` | `gardenStage: repot` | 🪴 Repotting |
|
| `#garden/repot` | `gardenStage: repot` | 🪴 Repotting |
|
||||||
| `#garden/revitalize` | `gardenStage: revitalize` | ✨ Revitalizing |
|
| `#garden/revitalize` | `gardenStage: revitalize` | ✨ Revitalizing |
|
||||||
@@ -84,14 +84,15 @@ The Eleventy blog renders a coloured badge on each post and groups all garden po
|
|||||||
---
|
---
|
||||||
title: "On building in public"
|
title: "On building in public"
|
||||||
tags:
|
tags:
|
||||||
- garden/cultivate
|
- garden/plant
|
||||||
|
category:
|
||||||
- indieweb
|
- indieweb
|
||||||
---
|
---
|
||||||
|
|
||||||
Some early thoughts on the merits of building in public...
|
Some early thoughts on the merits of building in public...
|
||||||
```
|
```
|
||||||
|
|
||||||
After publishing, the frontmatter gains:
|
After publishing, the frontmatter/property in Obsidian gains:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
mp-url: "https://example.com/articles/2026/on-building-in-public"
|
mp-url: "https://example.com/articles/2026/on-building-in-public"
|
||||||
@@ -108,7 +109,7 @@ mp-url: "https://example.com/articles/2026/on-building-in-public"
|
|||||||
| `title` | Sets the post `name` (article mode) |
|
| `title` | Sets the post `name` (article mode) |
|
||||||
| `created` / `date` | Sets `published` date (`created` takes priority — matches Obsidian's default date field) |
|
| `created` / `date` | Sets `published` date (`created` takes priority — matches Obsidian's default date field) |
|
||||||
| `postType` | Force post type: `article` sends a title (uses filename if none set), `note` skips title |
|
| `postType` | Force post type: `article` sends a title (uses filename if none set), `note` skips title |
|
||||||
| `tags` / `category` | Becomes Micropub `category` (excluding `garden/*` and bare `garden` tags) |
|
| `tags` + `category` | Both merged into Micropub `category` (excluding `garden/*` and bare `garden` tags, deduplicated) |
|
||||||
| `summary` / `excerpt` | Sets `summary` property |
|
| `summary` / `excerpt` | Sets `summary` property |
|
||||||
| `visibility` | `public` / `unlisted` / `private` |
|
| `visibility` | `public` / `unlisted` / `private` |
|
||||||
| `gardenStage` | Explicit garden stage — see table below |
|
| `gardenStage` | Explicit garden stage — see table below |
|
||||||
@@ -118,14 +119,14 @@ mp-url: "https://example.com/articles/2026/on-building-in-public"
|
|||||||
|
|
||||||
### AI disclosure properties
|
### AI disclosure properties
|
||||||
|
|
||||||
Use flat top-level properties for best Obsidian compatibility (Obsidian's Properties UI handles them more reliably than nested objects):
|
Use flat kebab-case properties (camelCase fallback supported for backward compatibility):
|
||||||
|
|
||||||
| Property | Values | Meaning |
|
| Property | Values | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `aiTextLevel` | `"0"` `"1"` `"2"` `"3"` | None / Editorial / Co-drafted / AI-generated |
|
| `ai-text-level` | `"0"` `"1"` `"2"` `"3"` | None / Editorial / Co-drafted / AI-generated |
|
||||||
| `aiCodeLevel` | `"0"` `"1"` `"2"` | None / AI-assisted / AI-generated |
|
| `ai-code-level` | `"0"` `"1"` `"2"` | None / AI-assisted / AI-generated |
|
||||||
| `aiTools` | string | Tools used, e.g. `"Claude"` |
|
| `ai-tools` | string | Tools used, e.g. `"Claude"` |
|
||||||
| `aiDescription` | string | Free-text disclosure note |
|
| `ai-description` | string | Free-text disclosure note |
|
||||||
|
|
||||||
Nested `ai:` objects (e.g. `ai: {textLevel: "1"}`) also work but flat keys are recommended.
|
Nested `ai:` objects (e.g. `ai: {textLevel: "1"}`) also work but flat keys are recommended.
|
||||||
|
|
||||||
@@ -137,11 +138,14 @@ title: "My Post"
|
|||||||
created: 2026-03-15T10:00:00
|
created: 2026-03-15T10:00:00
|
||||||
postType: article
|
postType: article
|
||||||
tags:
|
tags:
|
||||||
- garden/cultivate
|
- garden/evergreen
|
||||||
|
category:
|
||||||
- indieweb
|
- indieweb
|
||||||
aiTextLevel: "1"
|
- lang/en
|
||||||
aiCodeLevel: "0"
|
ai-text-level: "1"
|
||||||
aiTools: "Claude"
|
ai-code-level: "0"
|
||||||
|
ai-tools: "Claude"
|
||||||
|
ai-description: "AI helped refine the structure"
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+19
-16
@@ -137,14 +137,19 @@ export class Publisher {
|
|||||||
props["published"] = [new Date(String(rawDate)).toISOString()];
|
props["published"] = [new Date(String(rawDate)).toISOString()];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories from frontmatter `category` or `tags` (excluding garden/* tags)
|
// Categories from frontmatter `category` AND `tags` (excluding garden/* tags).
|
||||||
const rawTags = this.resolveArray(fm["tags"] ?? fm["category"]);
|
// Merge both fields — `tags` may contain garden/* stages while `category`
|
||||||
|
// holds the actual topic categories sent to Micropub.
|
||||||
|
const rawTags = [
|
||||||
|
...this.resolveArray(fm["tags"]),
|
||||||
|
...this.resolveArray(fm["category"]),
|
||||||
|
];
|
||||||
const gardenStageFromTags = this.extractGardenStage(rawTags);
|
const gardenStageFromTags = this.extractGardenStage(rawTags);
|
||||||
const normalTags = rawTags.filter(
|
const normalTags = rawTags.filter(
|
||||||
(t) => !t.startsWith(GARDEN_TAG_PREFIX) && t !== "garden",
|
(t) => !t.startsWith(GARDEN_TAG_PREFIX) && t !== "garden",
|
||||||
);
|
);
|
||||||
if (normalTags.length > 0) {
|
if (normalTags.length > 0) {
|
||||||
props["category"] = normalTags;
|
props["category"] = [...new Set(normalTags)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Garden stage — prefer explicit `gardenStage` frontmatter property,
|
// Garden stage — prefer explicit `gardenStage` frontmatter property,
|
||||||
@@ -178,22 +183,20 @@ export class Publisher {
|
|||||||
props["visibility"] = [visibility];
|
props["visibility"] = [visibility];
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI disclosure — flatten nested `ai` object into individual top-level
|
// AI disclosure — kebab-case keys (ai-text-level, ai-tools, etc.)
|
||||||
// properties so Indiekit writes them as plain scalar frontmatter keys.
|
// with camelCase fallback for backward compatibility.
|
||||||
// Also support top-level `aiTextLevel`, `aiTools`, etc. set directly.
|
// Also support nested `ai` object flattening.
|
||||||
// Sending `ai: [{textLevel: "1"}]` makes Indiekit write a YAML array,
|
|
||||||
// but the template reads `aiTextLevel` / `aiCodeLevel` as top-level scalars.
|
|
||||||
const aiObj = (fm["ai"] && typeof fm["ai"] === "object")
|
const aiObj = (fm["ai"] && typeof fm["ai"] === "object")
|
||||||
? fm["ai"] as Record<string, unknown>
|
? fm["ai"] as Record<string, unknown>
|
||||||
: {};
|
: {};
|
||||||
const aiTextLevel = fm["aiTextLevel"] ?? aiObj["textLevel"];
|
const aiTextLevel = fm["ai-text-level"] ?? fm["aiTextLevel"] ?? aiObj["textLevel"];
|
||||||
const aiCodeLevel = fm["aiCodeLevel"] ?? aiObj["codeLevel"];
|
const aiCodeLevel = fm["ai-code-level"] ?? fm["aiCodeLevel"] ?? aiObj["codeLevel"];
|
||||||
const aiTools = fm["aiTools"] ?? aiObj["aiTools"] ?? aiObj["tools"];
|
const aiTools = fm["ai-tools"] ?? fm["aiTools"] ?? aiObj["aiTools"] ?? aiObj["tools"];
|
||||||
const aiDescription = fm["aiDescription"] ?? aiObj["aiDescription"] ?? aiObj["description"];
|
const aiDescription = fm["ai-description"] ?? fm["aiDescription"] ?? aiObj["aiDescription"] ?? aiObj["description"];
|
||||||
if (aiTextLevel != null) props["aiTextLevel"] = [String(aiTextLevel)];
|
if (aiTextLevel != null) props["ai-text-level"] = [String(aiTextLevel)];
|
||||||
if (aiCodeLevel != null) props["aiCodeLevel"] = [String(aiCodeLevel)];
|
if (aiCodeLevel != null) props["ai-code-level"] = [String(aiCodeLevel)];
|
||||||
if (aiTools != null) props["aiTools"] = [String(aiTools)];
|
if (aiTools != null) props["ai-tools"] = [String(aiTools)];
|
||||||
if (aiDescription != null) props["aiDescription"] = [String(aiDescription)];
|
if (aiDescription != null) props["ai-description"] = [String(aiDescription)];
|
||||||
|
|
||||||
// Photos: prefer structured photo array from frontmatter (with alt text),
|
// Photos: prefer structured photo array from frontmatter (with alt text),
|
||||||
// fall back to uploaded local images.
|
// fall back to uploaded local images.
|
||||||
|
|||||||
Reference in New Issue
Block a user