diff --git a/package.json b/package.json index a92213a6..1aafd442 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-microsub-feed-discovery.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-webmention-sender-content-scope.mjs", - "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-microsub-feed-discovery.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-webmention-sender-content-scope.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", + "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-microsub-feed-discovery.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-webmention-sender-content-scope.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-endpoint-posts-search-tags.mjs", + "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-microsub-feed-discovery.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-webmention-sender-content-scope.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-endpoint-posts-search-tags.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/scripts/patch-endpoint-micropub-source-filter.mjs b/scripts/patch-endpoint-micropub-source-filter.mjs new file mode 100644 index 00000000..b7220529 --- /dev/null +++ b/scripts/patch-endpoint-micropub-source-filter.mjs @@ -0,0 +1,130 @@ +/** + * Patch: add `category` and `search` filtering to the Micropub `?q=source` + * list endpoint so that /posts can filter by tag and full-text search. + * + * When `category` is provided: + * filter MongoDB documents where `properties.category` matches the value. + * When `search` is provided: + * filter by a case-insensitive regex across name / content fields. + * + * Filtered queries bypass getCursor (no cursor-based pagination needed for + * small result sets) and return all matching posts up to the current limit. + */ +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-micropub/lib/controllers/query.js", + "node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-micropub/lib/controllers/query.js", + "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-micropub/lib/controllers/query.js", +]; + +const marker = "// filter-by-category-and-search patch"; + +const oldSnippet = ` } else { + // Return mf2 for published posts + let cursor = { + items: [], + hasNext: false, + hasPrev: false, + }; + + if (postsCollection) { + cursor = await getCursor(postsCollection, after, before, limit); + }`; + +const newSnippet = ` } else { + // Return mf2 for published posts + // filter-by-category-and-search patch + const categoryParam = request.query.category; + const searchParam = request.query.search; + const hasExtraFilter = Boolean(categoryParam || searchParam); + + let cursor = { + items: [], + hasNext: false, + hasPrev: false, + }; + + if (postsCollection) { + if (hasExtraFilter) { + const filterQuery = {}; + if (categoryParam) { + filterQuery["properties.category"] = String(categoryParam); + } + if (searchParam) { + const re = String(searchParam).replace( + /[$()*+.?[\\\]^{|}]/g, + "\\$&", + ); + filterQuery.$or = [ + { "properties.name": { $regex: re, $options: "i" } }, + { + "properties.content.text": { + $regex: re, + $options: "i", + }, + }, + { "properties.content": { $regex: re, $options: "i" } }, + ]; + } + const findLimit = (limit && limit > 0) ? limit : 40; + const filteredItems = await postsCollection + .find(filterQuery, { limit: findLimit, sort: { _id: -1 } }) + .toArray(); + cursor = { + items: filteredItems, + hasNext: false, + hasPrev: false, + }; + } else { + cursor = await getCursor(postsCollection, after, before, limit); + } + }`; + +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)) { + console.warn( + `[postinstall] Skipping micropub source-filter 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 micropub query controller files found"); +} else if (patched === 0) { + console.log("[postinstall] micropub source-filter patch already applied"); +} else { + console.log( + `[postinstall] Patched micropub source-filter in ${patched} file(s)`, + ); +} diff --git a/scripts/patch-endpoint-posts-search-tags.mjs b/scripts/patch-endpoint-posts-search-tags.mjs new file mode 100644 index 00000000..61853eb3 --- /dev/null +++ b/scripts/patch-endpoint-posts-search-tags.mjs @@ -0,0 +1,249 @@ +/** + * 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 %} +
+ {%- if activeCategory %} +Filtered by tag: {{ activeCategory }}
+ {%- endif %} + {%- if posts.length > 0 %} +