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