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:
@@ -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)`,
|
||||
);
|
||||
}
|
||||
@@ -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)`,
|
||||
);
|
||||
}
|
||||
@@ -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)`,
|
||||
);
|
||||
}
|
||||
@@ -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)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)`,
|
||||
);
|
||||
}
|
||||
@@ -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)`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user