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:
svemagie
2026-03-22 10:10:17 +01:00
parent c01496113d
commit a1afa653be
3 changed files with 41 additions and 34 deletions
+18 -14
View File
@@ -68,9 +68,9 @@ Tag any note in Obsidian with a `#garden/*` tag, or set `gardenStage` directly i
| Obsidian tag | Published property | Blog display |
|---|---|---|
| `#garden/plant` | `gardenStage: plant` | 🌱 Seedling |
| `#garden/cultivate` | `gardenStage: cultivate` | 🌿 Growing |
| `#garden/evergreen` | `gardenStage: evergreen` | 🌳 Evergreen |
| `#garden/cultivate` | `gardenStage: cultivate` | 🌿 Growing |
| `#garden/plant` | `gardenStage: plant` | 🌱 Seedling |
| `#garden/question` | `gardenStage: question` | ❓ Open Question |
| `#garden/repot` | `gardenStage: repot` | 🪴 Repotting |
| `#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"
tags:
- garden/cultivate
- garden/plant
category:
- indieweb
---
Some early thoughts on the merits of building in public...
```
After publishing, the frontmatter gains:
After publishing, the frontmatter/property in Obsidian gains:
```yaml
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) |
| `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 |
| `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 |
| `visibility` | `public` / `unlisted` / `private` |
| `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
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 |
|---|---|---|
| `aiTextLevel` | `"0"` `"1"` `"2"` `"3"` | None / Editorial / Co-drafted / AI-generated |
| `aiCodeLevel` | `"0"` `"1"` `"2"` | None / AI-assisted / AI-generated |
| `aiTools` | string | Tools used, e.g. `"Claude"` |
| `aiDescription` | string | Free-text disclosure note |
| `ai-text-level` | `"0"` `"1"` `"2"` `"3"` | None / Editorial / Co-drafted / AI-generated |
| `ai-code-level` | `"0"` `"1"` `"2"` | None / AI-assisted / AI-generated |
| `ai-tools` | string | Tools used, e.g. `"Claude"` |
| `ai-description` | string | Free-text disclosure note |
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
postType: article
tags:
- garden/cultivate
- garden/evergreen
category:
- indieweb
aiTextLevel: "1"
aiCodeLevel: "0"
aiTools: "Claude"
- lang/en
ai-text-level: "1"
ai-code-level: "0"
ai-tools: "Claude"
ai-description: "AI helped refine the structure"
---
```
+4 -4
View File
File diff suppressed because one or more lines are too long
+19 -16
View File
@@ -137,14 +137,19 @@ export class Publisher {
props["published"] = [new Date(String(rawDate)).toISOString()];
}
// Categories from frontmatter `category` or `tags` (excluding garden/* tags)
const rawTags = this.resolveArray(fm["tags"] ?? fm["category"]);
// Categories from frontmatter `category` AND `tags` (excluding garden/* tags).
// 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 normalTags = rawTags.filter(
(t) => !t.startsWith(GARDEN_TAG_PREFIX) && t !== "garden",
);
if (normalTags.length > 0) {
props["category"] = normalTags;
props["category"] = [...new Set(normalTags)];
}
// Garden stage — prefer explicit `gardenStage` frontmatter property,
@@ -178,22 +183,20 @@ export class Publisher {
props["visibility"] = [visibility];
}
// AI disclosure — flatten nested `ai` object into individual top-level
// properties so Indiekit writes them as plain scalar frontmatter keys.
// Also support top-level `aiTextLevel`, `aiTools`, etc. set directly.
// Sending `ai: [{textLevel: "1"}]` makes Indiekit write a YAML array,
// but the template reads `aiTextLevel` / `aiCodeLevel` as top-level scalars.
// AI disclosure — kebab-case keys (ai-text-level, ai-tools, etc.)
// with camelCase fallback for backward compatibility.
// Also support nested `ai` object flattening.
const aiObj = (fm["ai"] && typeof fm["ai"] === "object")
? fm["ai"] as Record<string, unknown>
: {};
const aiTextLevel = fm["aiTextLevel"] ?? aiObj["textLevel"];
const aiCodeLevel = fm["aiCodeLevel"] ?? aiObj["codeLevel"];
const aiTools = fm["aiTools"] ?? aiObj["aiTools"] ?? aiObj["tools"];
const aiDescription = fm["aiDescription"] ?? aiObj["aiDescription"] ?? aiObj["description"];
if (aiTextLevel != null) props["aiTextLevel"] = [String(aiTextLevel)];
if (aiCodeLevel != null) props["aiCodeLevel"] = [String(aiCodeLevel)];
if (aiTools != null) props["aiTools"] = [String(aiTools)];
if (aiDescription != null) props["aiDescription"] = [String(aiDescription)];
const aiTextLevel = fm["ai-text-level"] ?? fm["aiTextLevel"] ?? aiObj["textLevel"];
const aiCodeLevel = fm["ai-code-level"] ?? fm["aiCodeLevel"] ?? aiObj["codeLevel"];
const aiTools = fm["ai-tools"] ?? fm["aiTools"] ?? aiObj["aiTools"] ?? aiObj["tools"];
const aiDescription = fm["ai-description"] ?? fm["aiDescription"] ?? aiObj["aiDescription"] ?? aiObj["description"];
if (aiTextLevel != null) props["ai-text-level"] = [String(aiTextLevel)];
if (aiCodeLevel != null) props["ai-code-level"] = [String(aiCodeLevel)];
if (aiTools != null) props["ai-tools"] = [String(aiTools)];
if (aiDescription != null) props["ai-description"] = [String(aiDescription)];
// Photos: prefer structured photo array from frontmatter (with alt text),
// fall back to uploaded local images.