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({