From ae0b87940177ab93b1b222049c6f830a3935143c Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:38:23 +0100 Subject: [PATCH] fix: write like posts to GitHub store via postTemplate + store.createFile Previously sync only inserted into MongoDB, causing "file not found" errors when Indiekit tried to read the post. Now generates markdown via publication.postTemplate() and writes to GitHub via publication.store.createFile(), matching the micropub endpoint's create flow. Reset also deletes store files. Co-Authored-By: Claude Opus 4.6 --- lib/controllers/likes.js | 28 ++++++++++++++ lib/likes-sync.js | 82 ++++++++++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/lib/controllers/likes.js b/lib/controllers/likes.js index f7048be..7ff2fa5 100644 --- a/lib/controllers/likes.js +++ b/lib/controllers/likes.js @@ -236,8 +236,36 @@ export const likesController = { const postsCollection = request.app.locals.application.collections?.get("posts"); + const publication = request.app.locals.publication; + let deletedPosts = 0; if (postsCollection) { + // Delete files from the store (GitHub) before removing from MongoDB + if (publication?.store) { + const likePosts = await postsCollection + .find({ + "properties.post-type": "like", + "properties.youtube-video-id": { $exists: true }, + }) + .toArray(); + + for (const post of likePosts) { + try { + const message = publication.storeMessageTemplate + ? publication.storeMessageTemplate({ + action: "delete", + result: "deleted", + fileType: "post", + postType: "like", + }) + : `Delete like post ${post.path}`; + await publication.store.deleteFile(post.path, { message }); + } catch (err) { + console.error(`[YouTube] Failed to delete ${post.path} from store:`, err.message); + } + } + } + const result = await postsCollection.deleteMany({ "properties.post-type": "like", "properties.youtube-video-id": { $exists: true }, diff --git a/lib/likes-sync.js b/lib/likes-sync.js index 31cb7b7..bb47184 100644 --- a/lib/likes-sync.js +++ b/lib/likes-sync.js @@ -61,6 +61,28 @@ async function snapshotExistingLikes(db, client, accessToken, maxPages) { return count; } +/** + * Prepare template properties by stripping internal mp-* and post-type keys, + * matching what Indiekit's micropub endpoint does before calling postTemplate. + * @param {object} properties + * @returns {object} + */ +function getTemplateProperties(properties) { + const templateProperties = structuredClone(properties); + const preserveMpProperties = ["mp-syndicate-to"]; + + for (const key in templateProperties) { + if (key.startsWith("mp-") && !preserveMpProperties.includes(key)) { + delete templateProperties[key]; + } + if (key === "post-type") { + delete templateProperties[key]; + } + } + + return templateProperties; +} + /** * Sync liked videos into the Indiekit posts collection. * @@ -71,7 +93,7 @@ async function snapshotExistingLikes(db, client, accessToken, maxPages) { * @param {object} opts * @param {import("mongodb").Db} opts.db * @param {object} opts.youtubeConfig - endpoint options - * @param {object} opts.publication - Indiekit publication config + * @param {object} opts.publication - Indiekit publication (with store, postTemplate, storeMessageTemplate) * @param {import("mongodb").Collection} [opts.postsCollection] * @param {number} [opts.maxPages=3] - max pages to fetch (50 likes/page) * @returns {Promise<{synced: number, skipped: number, total: number, baselined?: number, error?: string}>} @@ -163,27 +185,47 @@ export async function syncLikes({ db, youtubeConfig, publication, postsCollectio ? likePostType.post.url.replace("{slug}", slug) : `${publicationUrl}/likes/${slug}/`; - const postDoc = { - path: postPath, - properties: { - "post-type": "like", - "mp-slug": slug, - "like-of": videoUrl, - name: `${video.title} - ${video.channelTitle}`, - content: { - text: `${video.title} - ${video.channelTitle}`, - html: `${escapeHtml(video.title)} - ${escapeHtml(video.channelTitle)}`, - }, - published: new Date().toISOString(), - url: postUrl, - visibility: "public", - "post-status": "draft", - "youtube-video-id": videoId, - "youtube-channel": video.channelTitle, - "youtube-thumbnail": video.thumbnail || "", + const postProperties = { + "post-type": "like", + "mp-slug": slug, + "like-of": videoUrl, + name: `${video.title} - ${video.channelTitle}`, + content: { + text: `${video.title} - ${video.channelTitle}`, + html: `${escapeHtml(video.title)} - ${escapeHtml(video.channelTitle)}`, }, + published: new Date().toISOString(), + url: postUrl, + visibility: "public", + "post-status": "draft", + "youtube-video-id": videoId, + "youtube-channel": video.channelTitle, + "youtube-thumbnail": video.thumbnail || "", }; + // Write markdown file to the store (e.g. GitHub) + if (publication?.postTemplate && publication?.store) { + try { + const templateProperties = getTemplateProperties(postProperties); + const content = await publication.postTemplate(templateProperties); + const message = publication.storeMessageTemplate + ? publication.storeMessageTemplate({ + action: "create", + result: "created", + fileType: "post", + postType: "like", + }) + : `Create like post for ${videoId}`; + + await publication.store.createFile(postPath, content, { message }); + } catch (storeError) { + console.error(`[YouTube] Failed to write ${postPath} to store:`, storeError.message); + // Continue — still insert into MongoDB so it isn't retried + } + } + + // Insert into MongoDB posts collection + const postDoc = { path: postPath, properties: postProperties }; if (postsCollection) { await postsCollection.insertOne(postDoc); } @@ -233,7 +275,7 @@ export function startLikesSync(Indiekit, options) { if (!db) return; const postsCollection = Indiekit.config?.application?.collections?.get("posts"); - const publication = Indiekit.config?.publication; + const publication = Indiekit.publication || Indiekit.config?.publication; try { const result = await syncLikes({