chore(ai): remove custom AI patches superseded by upstream endpoint-posts@beta.44

- Remove patch-preset-eleventy-ai-frontmatter: upstream now writes AI
  frontmatter natively using hyphenated keys (ai-text-level etc.)
- Remove patch-endpoint-posts-ai-cleanup: upstream beta.44 natively
  removes empty ai-text-level/ai-code-level/ai-tools/ai-description fields
- Remove patch-endpoint-posts-ai-fields: upstream beta.44 has AI form UI
  inline in post-form.njk; our separate templates would have duplicated fields
- Remove patch-micropub-ai-block-resync: one-time stale-block migration,
  no longer relevant
- Remove patch-endpoint-posts-prefill-url: upstream beta.44 has native
  prefill from query params; our patch would have conflicted
- Remove patch-endpoint-posts-search-tags: upstream beta.44 has native
  search/filter/sort UI; patch already detected this and was a no-op
- Bump @rmdes/indiekit-endpoint-posts beta.25→beta.44,
  override beta.41→beta.44
- Update indiekit.config.mjs: remove camelCase ai field names from
  all postTypes.fields (ai-* fields now rendered inline by upstream)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sven
2026-03-20 14:37:04 +01:00
parent b53afe2ed3
commit fe0f347e49
9 changed files with 17 additions and 1389 deletions

View File

@@ -1,120 +0,0 @@
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-posts/lib/controllers/form.js",
"node_modules/@indiekit/endpoint-posts/lib/controllers/form.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-posts/lib/controllers/form.js",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-posts/lib/controllers/form.js",
];
const marker = "Always remove legacy hyphenated keys — superseded by camelCase equivalents.";
const oldSnippet = [
" // Easy MDE appends `image` value to formData for last image uploaded",
" delete values.image;",
"",
" // Remove empty AI metadata fields so Micropub payload stays lean.",
" for (const key of [",
" \"aiTextLevel\",",
" \"aiCodeLevel\",",
" \"aiTools\",",
" \"aiDescription\",",
" \"ai-text-level\",",
" \"ai-code-level\",",
" \"ai-tools\",",
" \"ai-description\",",
" ]) {",
" if (",
" values[key] === undefined ||",
" values[key] === null ||",
" String(values[key]).trim() === \"\"",
" ) {",
" delete values[key];",
" }",
" }",
"",
" const mf2 = jf2ToMf2({ properties: sanitise(values) });",
].join("\n");
const newSnippet = [
" // Easy MDE appends `image` value to formData for last image uploaded",
" delete values.image;",
"",
" // Remove empty AI metadata fields so Micropub payload stays lean.",
" for (const key of [",
" \"aiTextLevel\",",
" \"aiCodeLevel\",",
" \"aiTools\",",
" \"aiDescription\",",
" ]) {",
" if (",
" values[key] === undefined ||",
" values[key] === null ||",
" String(values[key]).trim() === \"\"",
" ) {",
" delete values[key];",
" }",
" }",
" // Always remove legacy hyphenated keys — superseded by camelCase equivalents.",
" delete values[\"ai-text-level\"];",
" delete values[\"ai-code-level\"];",
" delete values[\"ai-tools\"];",
" delete values[\"ai-description\"];",
"",
" const mf2 = jf2ToMf2({ properties: sanitise(values) });",
].join("\n");
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
let checked = 0;
let patched = 0;
for (const filePath of candidates) {
if (!(await exists(filePath))) {
continue;
}
checked += 1;
const source = await readFile(filePath, "utf8");
if (source.includes(marker)) {
continue;
}
if (!source.includes(oldSnippet)) {
// Already has AI field cleanup in some form — skip silently
if (
source.includes('"ai-text-level"') ||
source.includes('"aiTextLevel"') ||
!source.includes("jf2ToMf2")
) {
continue;
}
console.warn(
`[postinstall] Skipping endpoint-posts AI cleanup patch for ${filePath}: upstream format changed`,
);
continue;
}
const updated = source.replace(oldSnippet, newSnippet);
await writeFile(filePath, updated, "utf8");
patched += 1;
}
if (checked === 0) {
console.log("[postinstall] No endpoint-posts form controller files found");
} else if (patched === 0) {
console.log("[postinstall] endpoint-posts AI cleanup patch already applied");
} else {
console.log(
`[postinstall] Patched endpoint-posts AI cleanup in ${patched} file(s)`,
);
}

View File

@@ -1,135 +0,0 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const endpointCandidates = [
"node_modules/@rmdes/indiekit-endpoint-posts",
"node_modules/@indiekit/endpoint-posts",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-posts",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-posts",
];
const templates = {
"aiTextLevel-field.njk": [
'{% set aiTextLevelValue = fieldData("aiTextLevel").value or fieldData("ai-text-level").value or (properties.ai.textLevel if properties.ai and properties.ai.textLevel is defined else properties.aiTextLevel) or properties["ai-text-level"] or "0" %}',
"{{ radios({",
' name: "aiTextLevel",',
" values: aiTextLevelValue,",
" fieldset: {",
' legend: "AI text level",',
" optional: true",
" },",
" items: [{",
' label: "0 - None",',
' value: "0"',
" }, {",
' label: "1 - Editorial assistance",',
' value: "1"',
" }, {",
' label: "2 - Co-drafting",',
' value: "2"',
" }, {",
' label: "3 - AI-generated (human reviewed)",',
' value: "3"',
" }]",
"}) }}",
].join("\n"),
"aiCodeLevel-field.njk": [
'{% set aiCodeLevelValue = fieldData("aiCodeLevel").value or fieldData("ai-code-level").value or (properties.ai.codeLevel if properties.ai and properties.ai.codeLevel is defined else properties.aiCodeLevel) or properties["ai-code-level"] or "0" %}',
"{{ radios({",
' name: "aiCodeLevel",',
" values: aiCodeLevelValue,",
" fieldset: {",
' legend: "AI code level",',
" optional: true",
" },",
" items: [{",
' label: "0 - Human-written",',
' value: "0"',
" }, {",
' label: "1 - AI-assisted",',
' value: "1"',
" }, {",
' label: "2 - Primarily AI-generated",',
' value: "2"',
" }]",
"}) }}",
].join("\n"),
"aiTools-field.njk": [
'{% set aiToolsValue = fieldData("aiTools").value or fieldData("ai-tools").value or (properties.ai.aiTools if properties.ai and properties.ai.aiTools is defined else properties.aiTools) or properties["ai-tools"] %}',
"{{ input({",
' name: "aiTools",',
" value: aiToolsValue,",
' label: "AI Tools",',
' hint: "Optional, comma-separated (e.g. Claude, ChatGPT, Copilot)",',
" optional: true",
"}) }}",
].join("\n"),
"aiDescription-field.njk": [
'{% set aiDescriptionValue = fieldData("aiDescription").value or fieldData("ai-description").value or (properties.ai.aiDescription if properties.ai and properties.ai.aiDescription is defined else properties.aiDescription) or properties["ai-description"] %}',
"{{ textarea({",
' name: "aiDescription",',
" value: aiDescriptionValue,",
' label: "AI usage note",',
' hint: "Optional: short note describing how AI was used",',
" optional: true",
"}) }}",
].join("\n"),
};
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
let checkedEndpoints = 0;
let checkedFiles = 0;
let patchedFiles = 0;
for (const endpointPath of endpointCandidates) {
if (!(await exists(endpointPath))) {
continue;
}
const includeDir = path.join(endpointPath, "includes", "post-types");
if (!(await exists(includeDir))) {
continue;
}
checkedEndpoints += 1;
await mkdir(includeDir, { recursive: true });
for (const [fileName, template] of Object.entries(templates)) {
checkedFiles += 1;
const filePath = path.join(includeDir, fileName);
const desired = `${template}\n`;
let current = "";
if (await exists(filePath)) {
current = await readFile(filePath, "utf8");
}
if (current === desired) {
continue;
}
await writeFile(filePath, desired, "utf8");
patchedFiles += 1;
}
}
if (checkedEndpoints === 0) {
console.log("[postinstall] No endpoint-posts package directories found");
} else if (checkedFiles === 0) {
console.log("[postinstall] No endpoint-posts AI field templates checked");
} else if (patchedFiles === 0) {
console.log("[postinstall] endpoint-posts AI field templates already patched");
} else {
console.log(
`[postinstall] Patched endpoint-posts AI field templates in ${patchedFiles}/${checkedFiles} file(s)`,
);
}

View File

@@ -1,98 +0,0 @@
/**
* Patch: pre-fill reference URL when creating posts from /news "Post" button.
*
* share-post.js opens /posts/create?type=like&url=<link>&name=<title>
* but postData.create only reads request.body for properties, ignoring query params.
*
* Fix: in postData.create, when properties is empty and request.query.url is present,
* seed properties with the correct field name for that post type:
* like → like-of
* bookmark → bookmark-of
* reply → in-reply-to
* repost → repost-of
* and optionally seed name/bookmark title from request.query.name.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const patchSpecs = [
{
name: "posts-prefill-url-from-query",
marker: "prefill reference URL from query param",
candidates: [
"node_modules/@rmdes/indiekit-endpoint-posts/lib/middleware/post-data.js",
"node_modules/@indiekit/endpoint-posts/lib/middleware/post-data.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-posts/lib/middleware/post-data.js",
],
oldSnippet: ` const postType = request.query.type || "note";
const properties = request.body || {};`,
newSnippet: ` const postType = request.query.type || "note";
// prefill reference URL from query param when opening from share-post button
let properties = request.body || {};
if (Object.entries(properties).length === 0 && request.query.url) {
const refUrl = request.query.url;
const refName = request.query.name || "";
const urlFieldByType = {
like: "like-of",
bookmark: "bookmark-of",
reply: "in-reply-to",
repost: "repost-of",
};
const urlField = urlFieldByType[postType];
if (urlField) {
properties = { [urlField]: refUrl };
if (postType === "bookmark" && refName) {
properties.name = refName;
}
}
}`,
},
];
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
let filesChecked = 0;
let filesPatched = 0;
for (const spec of patchSpecs) {
let foundAnyTarget = false;
for (const filePath of spec.candidates) {
if (!(await exists(filePath))) continue;
foundAnyTarget = true;
filesChecked += 1;
const source = await readFile(filePath, "utf8");
if (source.includes(spec.marker)) continue;
if (!source.includes(spec.oldSnippet)) {
console.log(`[postinstall] ${spec.name}: snippet not found in ${filePath}`);
continue;
}
const updated = source.replace(spec.oldSnippet, spec.newSnippet);
await writeFile(filePath, updated, "utf8");
filesPatched += 1;
}
if (!foundAnyTarget) {
console.log(`[postinstall] ${spec.name}: no target files found`);
}
}
if (filesChecked === 0) {
console.log("[postinstall] No posts endpoint post-data.js found");
} else if (filesPatched === 0) {
console.log("[postinstall] posts prefill-url already patched");
} else {
console.log(
`[postinstall] Patched posts prefill-url in ${filesPatched}/${filesChecked} file(s)`,
);
}

View File

@@ -1,299 +0,0 @@
/**
* Patch: add search input and tag chips to the /posts list page.
*
* Part A posts.js controller:
* - forward `category` and `search` query params to the Micropub source query
* - expose `item.tagLinks` (pre-encoded href + text) for each post
* - pass `activeCategory` and `activeSearch` to the template
*
* Part B posts.njk view:
* - replace the cardGrid call with a custom loop that appends clickable
* tag chips under each card
* - add a search form above the grid
*/
import { access, readFile, writeFile } from "node:fs/promises";
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
// ─── Part A: posts controller ────────────────────────────────────────────────
const controllerCandidates = [
"node_modules/@rmdes/indiekit-endpoint-posts/lib/controllers/posts.js",
"node_modules/@indiekit/endpoint-posts/lib/controllers/posts.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-posts/lib/controllers/posts.js",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-posts/lib/controllers/posts.js",
];
const controllerMarker = "// search-tags controller patch";
// 1) forward category + search to the micropub URL
const oldForward = ` const { after, before, success } = request.query;
const limit = Number(request.query.limit) || 12;
const micropubUrl = new URL(application.micropubEndpoint);
micropubUrl.searchParams.append("q", "source");
micropubUrl.searchParams.append("limit", String(limit));
if (after) {
micropubUrl.searchParams.append("after", String(after));
}
if (before) {
micropubUrl.searchParams.append("before", String(before));
}`;
const newForward = ` // search-tags controller patch
const { after, before, category, search, success } = request.query;
const limit = Number(request.query.limit) || 12;
const micropubUrl = new URL(application.micropubEndpoint);
micropubUrl.searchParams.append("q", "source");
micropubUrl.searchParams.append("limit", String(limit));
if (after) {
micropubUrl.searchParams.append("after", String(after));
}
if (before) {
micropubUrl.searchParams.append("before", String(before));
}
if (category) {
micropubUrl.searchParams.append("category", String(category));
}
if (search) {
micropubUrl.searchParams.append("search", String(search));
}`;
// 2) add tagLinks to each post item
const oldBadges = ` item.badges = getPostStatusBadges(item, response);
return item;`;
const newBadges = ` item.badges = getPostStatusBadges(item, response);
const rawTags = Array.isArray(item.category)
? item.category
: item.category
? [item.category]
: [];
item.tagLinks = rawTags.map((t) => ({
text: t,
href: \`?category=\${encodeURIComponent(t)}\`,
}));
return item;`;
// 3) pass activeCategory + activeSearch to the render call
const oldRender = ` statusTypes,
success,
});`;
const newRender = ` statusTypes,
success,
activeCategory: category || "",
activeSearch: search || "",
});`;
// ─── Part B: posts view ───────────────────────────────────────────────────────
const viewCandidates = [
"node_modules/@rmdes/indiekit-endpoint-posts/views/posts.njk",
"node_modules/@indiekit/endpoint-posts/views/posts.njk",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-posts/views/posts.njk",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-posts/views/posts.njk",
];
const viewMarker = "{# search-tags view patch #}";
const oldView = `{% extends "document.njk" %}
{% block content %}
{%- if posts.length > 0 %}
{{ cardGrid({
cardSize: "16rem",
items: posts
}) }}
{{ pagination(cursor) }}
{%- else -%}
{{ prose({ text: __("posts.posts.none") }) }}
{%- endif %}
{% endblock %}`;
const newView = `{% extends "document.njk" %}
{# search-tags view patch #}
{% block content %}
<form method="get" action="">
<div style="display:flex;gap:0.5rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1.5rem">
{{ input({
name: "search",
label: "Search",
value: activeSearch,
type: "search",
optional: true,
field: { classes: "-!-flex-grow" }
}) }}
{{ button({ text: "Search", type: "submit" }) }}
{%- if activeSearch or activeCategory %}
<a href="{{ parentUrl }}" class="button" style="align-self:flex-end">Clear</a>
{%- endif %}
</div>
</form>
{%- if activeCategory %}
<p style="margin-bottom:1rem">Filtered by tag: <strong>{{ activeCategory }}</strong></p>
{%- endif %}
{%- if posts.length > 0 %}
<ol class="card-grid" role="list" style="--min-card-size: 16rem">
{%- for item in posts %}
<li class="card-grid__item">
<article class="card">
{%- if item.photo %}
<div class="card__photo">
<img src="{{ item.photo.url | imageUrl(application, width=240, height=240) }}" alt="{{ item.photo.alt }}" width="240" height="240" decoding="async" loading="lazy" onerror="this.src='/assets/not-found.svg'">
</div>
{%- endif %}
<div class="card__body">
{%- if item.title %}
<h2 class="card__title">
{%- if item.url %}
<a href="{{ item.url }}" rel="bookmark">
{%- endif %}
{{- icon(item.icon) if item.icon -}}
{{- item.title | safe -}}
{%- if item.url %}
</a>
{%- endif %}
</h2>
{%- endif %}
{{ prose({ classes: "card__meta", text: item.description.text, html: item.description.html }) | indent(10) if item.description }}
{%- set hasNonGarden = false %}
{%- for tl in item.tagLinks %}
{%- if tl.text != "garden" %}{%- set hasNonGarden = true %}{%- endif %}
{%- endfor %}
{%- if hasNonGarden %}
<div style="padding:0.25rem 0 0.5rem;display:flex;flex-wrap:wrap;gap:0.25rem">
{%- for tl in item.tagLinks %}
{%- if tl.text != "garden" %}
<a href="{{ tl.href }}" class="tag" style="font-size:0.75em">{{ tl.text }}</a>
{%- endif %}
{%- endfor %}
</div>
{%- endif %}
{%- if item.published or item.badges %}
<footer class="card__footer" style="display:flex;align-items:center;gap:0.5rem">
{%- for badgeItem in item.badges %}
{{ badge(badgeItem) | indent(12) if badgeItem }}
{%- endfor %}
{%- for tl in item.tagLinks %}
{%- if tl.text == "garden" %}
<a href="{{ tl.href }}" class="tag" style="margin-left:auto;font-size:0.75em;flex-shrink:0">garden</a>
{%- endif %}
{%- endfor %}
<time datetime="{{ item.published }}">
{{ item.published | date("PPp", { locale: item.locale, timeZone: application.timeZone }) }}
</time>
</footer>
{%- endif %}
</div>
</article>
</li>
{%- endfor %}
</ol>
{{ pagination(cursor) }}
{%- else -%}
{{ prose({ text: __("posts.posts.none") }) }}
{%- endif %}
{% endblock %}`;
// ─── Apply patches ────────────────────────────────────────────────────────────
let controllerChecked = 0;
let controllerPatched = 0;
for (const filePath of controllerCandidates) {
if (!(await exists(filePath))) continue;
controllerChecked += 1;
let source = await readFile(filePath, "utf8");
if (source.includes(controllerMarker)) continue;
let changed = false;
for (const [oldSnip, newSnip] of [
[oldForward, newForward],
[oldBadges, newBadges],
[oldRender, newRender],
]) {
if (!source.includes(oldSnip)) {
// Beta.41+ has native filter/search/sort built in — skip silently
if (source.includes("buildFilterQuery") || source.includes("filters.postType")) {
changed = false;
break;
}
console.warn(
`[postinstall] posts search-tags: snippet not found in ${filePath}, skipping`,
);
changed = false;
break;
}
source = source.replace(oldSnip, newSnip);
changed = true;
}
if (changed) {
await writeFile(filePath, source, "utf8");
controllerPatched += 1;
}
}
let viewChecked = 0;
let viewPatched = 0;
for (const filePath of viewCandidates) {
if (!(await exists(filePath))) continue;
viewChecked += 1;
const source = await readFile(filePath, "utf8");
if (source.includes(viewMarker)) continue;
if (!source.includes(oldView)) {
// Beta.41+ has native filter/sort UI built in — skip silently
if (source.includes("posts-filter-row") || source.includes("posts.filter.type")) {
continue;
}
console.warn(
`[postinstall] posts search-tags: view not found in ${filePath}, skipping`,
);
continue;
}
const updated = source.replace(oldView, newView);
await writeFile(filePath, updated, "utf8");
viewPatched += 1;
}
if (controllerChecked === 0 && viewChecked === 0) {
console.log("[postinstall] No endpoint-posts files found");
} else {
if (controllerPatched === 0 && controllerChecked > 0) {
console.log("[postinstall] posts search-tags controller already patched");
} else if (controllerPatched > 0) {
console.log(
`[postinstall] Patched posts search-tags controller in ${controllerPatched} file(s)`,
);
}
if (viewPatched === 0 && viewChecked > 0) {
console.log("[postinstall] posts search-tags view already patched");
} else if (viewPatched > 0) {
console.log(
`[postinstall] Patched posts search-tags view in ${viewPatched} file(s)`,
);
}
}

View File

@@ -1,115 +0,0 @@
/**
* Patch @indiekit/endpoint-micropub/lib/post-data.js to detect stale AI block files.
*
* Problem: The v3 patch bug (supportsAiDisclosure always false) caused Indiekit to update
* MongoDB with AI field values (aiTextLevel, aiCodeLevel, etc.) but write the post file
* WITHOUT the ai: frontmatter block. Now when the user re-saves with the same AI values,
* Indiekit's isDeepStrictEqual check says "no properties changed" and skips the file write.
* The file remains stale (missing ai: block) even though MongoDB has the right data.
*
* Fix: Store an `_aiBlockVersion` field in MongoDB alongside each post. On update, if the
* stored version doesn't match the current patch version AND the post has AI fields, bypass
* the no-change check and force a file re-write. This triggers exactly once per affected
* post, then every subsequent no-change save correctly skips the write.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const AI_BLOCK_VERSION = "v4";
const candidates = [
"node_modules/@indiekit/endpoint-micropub/lib/post-data.js",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/post-data.js",
];
const marker = "AI block version resync patch";
// --- Old: simple destructuring that ignores _aiBlockVersion ---
const oldDestructure = `let { path: _originalPath, properties } = await this.read(application, url);`;
const newDestructure = `let { path: _originalPath, properties, _aiBlockVersion: storedAiBlockVersion } = await this.read(application, url); // AI block version resync patch`;
// --- Old: early return when no properties changed ---
const oldNoChange = ` // Return if no changes to template properties detected
const newProperties = getPostTemplateProperties(properties);
oldProperties = getPostTemplateProperties(oldProperties);
if (isDeepStrictEqual(newProperties, oldProperties)) {
return;
}`;
const newNoChange = ` // Return if no changes to template properties detected
const newProperties = getPostTemplateProperties(properties);
oldProperties = getPostTemplateProperties(oldProperties);
if (isDeepStrictEqual(newProperties, oldProperties)) {
// AI block version resync patch: if post has AI fields and the file was written by an
// older patch version (or never written with the ai: block), force a one-time re-write.
const hasAiFields =
newProperties.aiTextLevel !== undefined ||
newProperties.aiCodeLevel !== undefined;
const currentAiBlockVersion = "${AI_BLOCK_VERSION}";
if (!hasAiFields || storedAiBlockVersion === currentAiBlockVersion) {
return;
}
// Fall through: force re-write to fix stale ai: block
}`;
// --- Old: postData construction without _aiBlockVersion ---
const oldPostData = ` // Update data in posts collection
const postData = { _originalPath, path, properties };`;
const newPostData = ` // Update data in posts collection
const postData = { _originalPath, path, properties, _aiBlockVersion: "${AI_BLOCK_VERSION}" }; // AI block version resync patch`;
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
let checked = 0;
let patched = 0;
for (const filePath of candidates) {
if (!(await exists(filePath))) {
continue;
}
checked += 1;
const source = await readFile(filePath, "utf8");
if (source.includes(marker)) {
continue;
}
if (
!source.includes(oldDestructure) ||
!source.includes(oldNoChange) ||
!source.includes(oldPostData)
) {
console.warn(
`[postinstall] Skipping micropub AI block resync patch for ${filePath}: upstream format changed`,
);
continue;
}
const updated = source
.replace(oldDestructure, newDestructure)
.replace(oldNoChange, newNoChange)
.replace(oldPostData, newPostData);
await writeFile(filePath, updated, "utf8");
patched += 1;
}
if (checked === 0) {
console.log("[postinstall] No endpoint-micropub post-data.js found");
} else if (patched === 0) {
console.log("[postinstall] micropub AI block resync patch already applied");
} else {
console.log(
`[postinstall] Patched micropub AI block resync in ${patched} file(s)`,
);
}

View File

@@ -1,577 +0,0 @@
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-preset-eleventy/lib/post-template.js",
"node_modules/@indiekit/preset-eleventy/lib/post-template.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-preset-eleventy/lib/post-template.js",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/preset-eleventy/lib/post-template.js",
];
const patchMarker =
"Indiekit removes post-type before calling postTemplate; fall back to permalink-based detection.";
const upstreamBlock = [
" // Convert url to Eleventy permalink so generated URL matches Indiekit's stored URL",
" // Add trailing slash to generate /path/index.html instead of /path.html",
" if (properties.url) {",
" const url = properties.url;",
" properties.permalink = url.endsWith(\"/\") ? url : `${url}/`;",
" }",
" delete properties.url;",
"",
" const frontMatter = YAML.stringify(properties, { lineWidth: 0 });",
" return `---\\n${frontMatter}---\\n`;",
"};",
].join("\n");
const v1PatchedBlock = [
" // Convert url to Eleventy permalink so generated URL matches Indiekit's stored URL",
" // Add trailing slash to generate /path/index.html instead of /path.html",
" if (properties.url) {",
" const url = properties.url;",
" properties.permalink = url.endsWith(\"/\") ? url : `${url}/`;",
" }",
" delete properties.url;",
"",
" // Normalize AI disclosure metadata and default to no AI usage.",
" const aiSource =",
" properties.ai && typeof properties.ai === \"object\" && !Array.isArray(properties.ai)",
" ? properties.ai",
" : {};",
"",
" const aiTextLevel = String(",
" aiSource.textLevel ?? aiSource.aiTextLevel ?? properties.aiTextLevel ?? \"0\",",
" );",
"",
" const aiCodeLevel = String(",
" aiSource.codeLevel ?? aiSource.aiCodeLevel ?? properties.aiCodeLevel ?? \"0\",",
" );",
"",
" const aiTools = aiSource.aiTools ?? aiSource.tools ?? properties.aiTools;",
"",
" const aiDescription =",
" aiSource.aiDescription ?? aiSource.description ?? properties.aiDescription;",
"",
" delete properties.ai;",
" delete properties.aiTextLevel;",
" delete properties.aiCodeLevel;",
" delete properties.aiTools;",
" delete properties.aiDescription;",
"",
" const frontMatter = YAML.stringify(properties, { lineWidth: 0 });",
"",
" let aiFrontMatter = `ai:\\n textLevel: \\\"${aiTextLevel}\\\"\\n codeLevel: \\\"${aiCodeLevel}\\\"\\n # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n`;",
"",
" if (aiTools !== undefined && aiTools !== null && aiTools !== \"\") {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n',",
" ` aiTools: ${JSON.stringify(String(aiTools))}\\n`,",
" );",
" }",
"",
" if (aiDescription !== undefined && aiDescription !== null && aiDescription !== \"\") {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n',",
" ` aiDescription: ${JSON.stringify(String(aiDescription))}\\n`,",
" );",
" }",
"",
" return `---\\n${frontMatter}${aiFrontMatter}---\\n`;",
"};",
].join("\n");
const v2PatchedBlock = [
" // Convert url to Eleventy permalink so generated URL matches Indiekit's stored URL",
" // Add trailing slash to generate /path/index.html instead of /path.html",
" if (properties.url) {",
" const url = properties.url;",
" properties.permalink = url.endsWith(\"/\") ? url : `${url}/`;",
" }",
" delete properties.url;",
"",
" // Normalize AI disclosure metadata for articles and notes only, defaulting to no AI usage.",
" const aiSource =",
" properties.ai && typeof properties.ai === \"object\" && !Array.isArray(properties.ai)",
" ? properties.ai",
" : {};",
"",
" const aiTextLevel = String(",
" aiSource.textLevel ?? aiSource.aiTextLevel ?? properties.aiTextLevel ?? \"0\",",
" );",
"",
" const aiCodeLevel = String(",
" aiSource.codeLevel ?? aiSource.aiCodeLevel ?? properties.aiCodeLevel ?? \"0\",",
" );",
"",
" const aiTools = aiSource.aiTools ?? aiSource.tools ?? properties.aiTools;",
"",
" const aiDescription =",
" aiSource.aiDescription ?? aiSource.description ?? properties.aiDescription;",
"",
" delete properties.ai;",
" delete properties.aiTextLevel;",
" delete properties.aiCodeLevel;",
" delete properties.aiTools;",
" delete properties.aiDescription;",
"",
" const postType = String(",
" properties.postType ?? properties[\"post-type\"] ?? \"\",",
" ).toLowerCase();",
" const supportsAiDisclosure = postType === \"article\" || postType === \"note\";",
"",
" const frontMatter = YAML.stringify(properties, { lineWidth: 0 });",
"",
" if (!supportsAiDisclosure) {",
" return `---\\n${frontMatter}---\\n`;",
" }",
"",
" let aiFrontMatter = `ai:\\n textLevel: \\\"${aiTextLevel}\\\"\\n codeLevel: \\\"${aiCodeLevel}\\\"\\n # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n`;",
"",
" if (aiTools !== undefined && aiTools !== null && aiTools !== \"\") {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n',",
" ` aiTools: ${JSON.stringify(String(aiTools))}\\n`,",
" );",
" }",
"",
" if (aiDescription !== undefined && aiDescription !== null && aiDescription !== \"\") {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n',",
" ` aiDescription: ${JSON.stringify(String(aiDescription))}\\n`,",
" );",
" }",
"",
" return `---\\n${frontMatter}${aiFrontMatter}---\\n`;",
"};",
].join("\n");
const v3Block = [
" // Convert url to Eleventy permalink so generated URL matches Indiekit's stored URL",
" // Add trailing slash to generate /path/index.html instead of /path.html",
" if (properties.url) {",
" const url = properties.url;",
" properties.permalink = url.endsWith(\"/\") ? url : `${url}/`;",
" }",
" delete properties.url;",
"",
" // Normalize and sanitize AI disclosure metadata for articles and notes only.",
" const aiSource =",
" properties.ai && typeof properties.ai === \"object\" && !Array.isArray(properties.ai)",
" ? properties.ai",
" : {};",
"",
" const normaliseString = (value) => {",
" if (value === undefined || value === null) {",
" return undefined;",
" }",
"",
" const text = String(value).trim();",
" return text === \"\" ? undefined : text;",
" };",
"",
" const normaliseLevel = (value, allowedValues, fallback = \"0\") => {",
" const candidate = normaliseString(value);",
"",
" if (!candidate) {",
" return fallback;",
" }",
"",
" return allowedValues.includes(candidate) ? candidate : fallback;",
" };",
"",
" const aiTextLevelRaw =",
" aiSource.textLevel ??",
" aiSource.aiTextLevel ??",
" properties.aiTextLevel ??",
" properties[\"ai-text-level\"] ??",
" \"0\";",
"",
" const aiCodeLevelRaw =",
" aiSource.codeLevel ??",
" aiSource.aiCodeLevel ??",
" properties.aiCodeLevel ??",
" properties[\"ai-code-level\"] ??",
" \"0\";",
"",
" const aiTextLevel = normaliseLevel(aiTextLevelRaw, [\"0\", \"1\", \"2\", \"3\"]);",
" // Legacy value \"3\" is folded into \"2\" for code-level taxonomy compatibility.",
" const aiCodeLevel = normaliseLevel(",
" aiCodeLevelRaw === \"3\" ? \"2\" : aiCodeLevelRaw,",
" [\"0\", \"1\", \"2\"],",
" );",
"",
" const aiTools = normaliseString(",
" aiSource.aiTools ?? aiSource.tools ?? properties.aiTools ?? properties[\"ai-tools\"],",
" );",
"",
" const aiDescription = normaliseString(",
" aiSource.aiDescription ??",
" aiSource.description ??",
" properties.aiDescription ??",
" properties[\"ai-description\"],",
" );",
"",
" delete properties.ai;",
" delete properties.aiTextLevel;",
" delete properties.aiCodeLevel;",
" delete properties.aiTools;",
" delete properties.aiDescription;",
" delete properties[\"ai-text-level\"];",
" delete properties[\"ai-code-level\"];",
" delete properties[\"ai-tools\"];",
" delete properties[\"ai-description\"];",
"",
" const postType = String(",
" properties.postType ?? properties[\"post-type\"] ?? properties.type ?? \"\",",
" ).toLowerCase();",
" const supportsAiDisclosure = postType === \"article\" || postType === \"note\";",
"",
" const frontMatter = YAML.stringify(properties, { lineWidth: 0 });",
"",
" if (!supportsAiDisclosure) {",
" return `---\\n${frontMatter}---\\n`;",
" }",
"",
" let aiFrontMatter = `ai:\\n textLevel: \\\"${aiTextLevel}\\\"\\n codeLevel: \\\"${aiCodeLevel}\\\"\\n # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n`;",
"",
" if (aiTools) {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n',",
" ` aiTools: ${JSON.stringify(aiTools)}\\n`,",
" );",
" }",
"",
" if (aiDescription) {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n',",
" ` aiDescription: ${JSON.stringify(aiDescription)}\\n`,",
" );",
" }",
"",
" return `---\\n${frontMatter}${aiFrontMatter}---\\n`;",
"};",
].join("\n");
// v4: fix post-type detection — Indiekit removes post-type before calling postTemplate,
// so fall back to permalink URL pattern to detect articles and notes.
const v4Block = [
" // Convert url to Eleventy permalink so generated URL matches Indiekit's stored URL",
" // Add trailing slash to generate /path/index.html instead of /path.html",
" if (properties.url) {",
" const url = properties.url;",
" properties.permalink = url.endsWith(\"/\") ? url : `${url}/`;",
" }",
" delete properties.url;",
"",
" // Normalize and sanitize AI disclosure metadata for articles and notes only.",
" const aiSource =",
" properties.ai && typeof properties.ai === \"object\" && !Array.isArray(properties.ai)",
" ? properties.ai",
" : {};",
"",
" const normaliseString = (value) => {",
" if (value === undefined || value === null) {",
" return undefined;",
" }",
"",
" const text = String(value).trim();",
" return text === \"\" ? undefined : text;",
" };",
"",
" const normaliseLevel = (value, allowedValues, fallback = \"0\") => {",
" const candidate = normaliseString(value);",
"",
" if (!candidate) {",
" return fallback;",
" }",
"",
" return allowedValues.includes(candidate) ? candidate : fallback;",
" };",
"",
" const aiTextLevelRaw =",
" aiSource.textLevel ??",
" aiSource.aiTextLevel ??",
" properties.aiTextLevel ??",
" properties[\"ai-text-level\"] ??",
" \"0\";",
"",
" const aiCodeLevelRaw =",
" aiSource.codeLevel ??",
" aiSource.aiCodeLevel ??",
" properties.aiCodeLevel ??",
" properties[\"ai-code-level\"] ??",
" \"0\";",
"",
" const aiTextLevel = normaliseLevel(aiTextLevelRaw, [\"0\", \"1\", \"2\", \"3\"]);",
" // Legacy value \"3\" is folded into \"2\" for code-level taxonomy compatibility.",
" const aiCodeLevel = normaliseLevel(",
" aiCodeLevelRaw === \"3\" ? \"2\" : aiCodeLevelRaw,",
" [\"0\", \"1\", \"2\"],",
" );",
"",
" const aiTools = normaliseString(",
" aiSource.aiTools ?? aiSource.tools ?? properties.aiTools ?? properties[\"ai-tools\"],",
" );",
"",
" const aiDescription = normaliseString(",
" aiSource.aiDescription ??",
" aiSource.description ??",
" properties.aiDescription ??",
" properties[\"ai-description\"],",
" );",
"",
" delete properties.ai;",
" delete properties.aiTextLevel;",
" delete properties.aiCodeLevel;",
" delete properties.aiTools;",
" delete properties.aiDescription;",
" delete properties[\"ai-text-level\"];",
" delete properties[\"ai-code-level\"];",
" delete properties[\"ai-tools\"];",
" delete properties[\"ai-description\"];",
"",
" // Indiekit removes post-type before calling postTemplate; fall back to permalink-based detection.",
" const postType = String(",
" properties.postType ?? properties[\"post-type\"] ?? properties.type ?? \"\",",
" ).toLowerCase();",
" const permalink = String(properties.permalink ?? \"\");",
" const supportsAiDisclosure =",
" postType === \"article\" || postType === \"note\" ||",
" /\\/articles(?:\\/|$)/.test(permalink) || /\\/notes(?:\\/|$)/.test(permalink);",
"",
" const frontMatter = YAML.stringify(properties, { lineWidth: 0 });",
"",
" if (!supportsAiDisclosure) {",
" return `---\\n${frontMatter}---\\n`;",
" }",
"",
" let aiFrontMatter = `ai:\\n textLevel: \\\"${aiTextLevel}\\\"\\n codeLevel: \\\"${aiCodeLevel}\\\"\\n # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n`;",
"",
" if (aiTools) {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n',",
" ` aiTools: ${JSON.stringify(aiTools)}\\n`,",
" );",
" }",
"",
" if (aiDescription) {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n',",
" ` aiDescription: ${JSON.stringify(aiDescription)}\\n`,",
" );",
" }",
"",
" return `---\\n${frontMatter}${aiFrontMatter}---\\n`;",
"};",
].join("\n");
// v5: upstream added mpUrl storage + URL normalization (pathname extraction).
// Matches the new block structure added after v4.
const v5UpstreamBlock = [
" // Store the Micropub URL for frontend edit links before deleting it",
" if (properties.url) {",
" properties.mpUrl = properties.url;",
" }",
"",
" // Convert Indiekit URL to Eleventy permalink so pages generate",
" // at the canonical URL (e.g., /notes/2026/02/22/slug/) instead of",
" // the file-path-based URL (e.g., /content/notes/2026-02-22-slug/).",
" if (properties.url) {",
" let url = properties.url;",
" if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) {",
" try {",
" url = new URL(url).pathname;",
" } catch {",
" // If URL parsing fails, use as-is",
" }",
" }",
" properties.permalink = url.endsWith(\"/\") ? url : `${url}/`;",
" }",
" delete properties.url;",
"",
" const frontMatter = YAML.stringify(properties, { lineWidth: 0 });",
" return `---\\n${frontMatter}---\\n`;",
"};",
].join("\n");
const v5PatchedBlock = [
" // Store the Micropub URL for frontend edit links before deleting it",
" if (properties.url) {",
" properties.mpUrl = properties.url;",
" }",
"",
" // Convert Indiekit URL to Eleventy permalink so pages generate",
" // at the canonical URL (e.g., /notes/2026/02/22/slug/) instead of",
" // the file-path-based URL (e.g., /content/notes/2026-02-22-slug/).",
" if (properties.url) {",
" let url = properties.url;",
" if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) {",
" try {",
" url = new URL(url).pathname;",
" } catch {",
" // If URL parsing fails, use as-is",
" }",
" }",
" properties.permalink = url.endsWith(\"/\") ? url : `${url}/`;",
" }",
" delete properties.url;",
"",
" // Normalize and sanitize AI disclosure metadata for articles and notes only.",
" const aiSource =",
" properties.ai && typeof properties.ai === \"object\" && !Array.isArray(properties.ai)",
" ? properties.ai",
" : {};",
"",
" const normaliseString = (value) => {",
" if (value === undefined || value === null) {",
" return undefined;",
" }",
"",
" const text = String(value).trim();",
" return text === \"\" ? undefined : text;",
" };",
"",
" const normaliseLevel = (value, allowedValues, fallback = \"0\") => {",
" const candidate = normaliseString(value);",
"",
" if (!candidate) {",
" return fallback;",
" }",
"",
" return allowedValues.includes(candidate) ? candidate : fallback;",
" };",
"",
" const aiTextLevelRaw =",
" aiSource.textLevel ??",
" aiSource.aiTextLevel ??",
" properties.aiTextLevel ??",
" properties[\"ai-text-level\"] ??",
" \"0\";",
"",
" const aiCodeLevelRaw =",
" aiSource.codeLevel ??",
" aiSource.aiCodeLevel ??",
" properties.aiCodeLevel ??",
" properties[\"ai-code-level\"] ??",
" \"0\";",
"",
" const aiTextLevel = normaliseLevel(aiTextLevelRaw, [\"0\", \"1\", \"2\", \"3\"]);",
" // Legacy value \"3\" is folded into \"2\" for code-level taxonomy compatibility.",
" const aiCodeLevel = normaliseLevel(",
" aiCodeLevelRaw === \"3\" ? \"2\" : aiCodeLevelRaw,",
" [\"0\", \"1\", \"2\"],",
" );",
"",
" const aiTools = normaliseString(",
" aiSource.aiTools ?? aiSource.tools ?? properties.aiTools ?? properties[\"ai-tools\"],",
" );",
"",
" const aiDescription = normaliseString(",
" aiSource.aiDescription ??",
" aiSource.description ??",
" properties.aiDescription ??",
" properties[\"ai-description\"],",
" );",
"",
" delete properties.ai;",
" delete properties.aiTextLevel;",
" delete properties.aiCodeLevel;",
" delete properties.aiTools;",
" delete properties.aiDescription;",
" delete properties[\"ai-text-level\"];",
" delete properties[\"ai-code-level\"];",
" delete properties[\"ai-tools\"];",
" delete properties[\"ai-description\"];",
"",
" // Indiekit removes post-type before calling postTemplate; fall back to permalink-based detection.",
" const postType = String(",
" properties.postType ?? properties[\"post-type\"] ?? properties.type ?? \"\",",
" ).toLowerCase();",
" const permalink = String(properties.permalink ?? \"\");",
" const supportsAiDisclosure =",
" postType === \"article\" || postType === \"note\" ||",
" /\\/articles(?:\\/|$)/.test(permalink) || /\\/notes(?:\\/|$)/.test(permalink);",
"",
" const frontMatter = YAML.stringify(properties, { lineWidth: 0 });",
"",
" if (!supportsAiDisclosure) {",
" return `---\\n${frontMatter}---\\n`;",
" }",
"",
" let aiFrontMatter = `ai:\\n textLevel: \\\"${aiTextLevel}\\\"\\n codeLevel: \\\"${aiCodeLevel}\\\"\\n # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n`;",
"",
" if (aiTools) {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiTools: \\\"Claude, ChatGPT, Copilot\\\"\\n',",
" ` aiTools: ${JSON.stringify(aiTools)}\\n`,",
" );",
" }",
"",
" if (aiDescription) {",
" aiFrontMatter = aiFrontMatter.replace(",
" ' # aiDescription: \\\"Optional disclosure about how AI was used\\\"\\n',",
" ` aiDescription: ${JSON.stringify(aiDescription)}\\n`,",
" );",
" }",
"",
" return `---\\n${frontMatter}${aiFrontMatter}---\\n`;",
"};",
].join("\n");
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
let checked = 0;
let patched = 0;
for (const filePath of candidates) {
if (!(await exists(filePath))) {
continue;
}
checked += 1;
const source = await readFile(filePath, "utf8");
if (source.includes(patchMarker)) {
continue;
}
let updated = source;
if (source.includes(v5UpstreamBlock)) {
updated = source.replace(v5UpstreamBlock, v5PatchedBlock);
} else if (source.includes(v3Block)) {
updated = source.replace(v3Block, v4Block);
} else if (source.includes(v2PatchedBlock)) {
updated = source.replace(v2PatchedBlock, v4Block);
} else if (source.includes(v1PatchedBlock)) {
updated = source.replace(v1PatchedBlock, v4Block);
} else if (source.includes(upstreamBlock)) {
updated = source.replace(upstreamBlock, v4Block);
} else {
console.warn(
`[postinstall] Skipping preset-eleventy AI frontmatter patch for ${filePath}: upstream format changed`,
);
continue;
}
await writeFile(filePath, updated, "utf8");
patched += 1;
}
if (checked === 0) {
console.log("[postinstall] No preset-eleventy post-template files found");
} else if (patched === 0) {
console.log("[postinstall] preset-eleventy AI frontmatter patch already applied");
} else {
console.log(
`[postinstall] Patched preset-eleventy AI frontmatter in ${patched} file(s)`,
);
}