Repository: getindiekit/indiekit
Pull request #680 (2024-01-10 23:10:42)

Update Jekyll and Hugo post templates to accept unknown properties 
Currently, the provided preset plugins dictate the post properties that will be output in a post’s front matter. And yet, we allow users to add different post types that, down the line, could output different properties.

This PR updates the 2 preset plugins, adjusting only the properties that need adjusting, and thus allowing unknown properties to be output. This also means these presets don’t need updating every time there is a change to the posts and properties produced by the/a Micropub plugin.

Also, this PR prevents `mp-*` properties from being sent to the `postTemplate()` function; these are only intended for sending commands to Micropub servers, and should not be used in posts. 


Patch:
diff --git a/helpers/fixtures/jf2/post-template-properties.jf2 b/helpers/fixtures/jf2/post-template-properties.jf2
new file mode 100644
index 00000000..8791840c
--- /dev/null
+++ b/helpers/fixtures/jf2/post-template-properties.jf2
@@ -0,0 +1,42 @@
+{
+  "type": "entry",
+  "url": "https://website.example/posts/cheese-sandwich",
+  "name": "What I had for lunch",
+  "content": {
+    "html": "<p>I ate a <a href=\"https://en.wikipedia.org/wiki/Cheese\">cheese</a> sandwich, which was nice.</p>",
+    "text": "I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice."
+  },
+  "summary": "A very satisfactory meal.",
+  "published": "2020-02-02",
+  "category": ["lunch", "food"],
+  "audio": [{
+    "url": "https://website.example/audio.mp3"
+  }],
+  "photo": [{
+    "alt": "Alternative text",
+    "url": "https://website.example/photo.jpg"
+  }],
+  "video": [{
+    "url": "https://website.example/video.mp4"
+  }],
+  "start": "2020-02-02",
+  "end": "2020-02-20",
+  "rsvp": "Yes",
+  "location": {
+    "type": "geo",
+    "latitude": "37.780080",
+    "longitude": "-122.420160",
+    "name": "37° 46′ 48.29″ N 122° 25′ 12.576″ W"
+  },
+  "checkin": {
+    "type": "card",
+    "latitude": "50",
+    "longitude": "0"
+  },
+  "bookmark-of": "https://website.example",
+  "like-of": "https://website.example",
+  "repost-of": "https://website.example",
+  "in-reply-to": "https://website.example",
+  "visibility": "private",
+  "syndication": "https://website.example/post/12345"
+}
diff --git a/packages/endpoint-micropub/lib/post-content.js b/packages/endpoint-micropub/lib/post-content.js
index 04627baa..8a7e8e36 100644
--- a/packages/endpoint-micropub/lib/post-content.js
+++ b/packages/endpoint-micropub/lib/post-content.js
@@ -14,7 +14,13 @@ export const postContent = {
       fileType: "post",
       postType: properties["post-type"],
     };
-    const content = await postTemplate(properties);
+
+    // Remove server commands from post template properties
+    const templateProperties = Object.keys(properties).filter(
+      (key) => !key.startsWith("mp-"),
+    );
+
+    const content = await postTemplate(templateProperties);
     const message = storeMessageTemplate(metaData);
 
     await store.createFile(path, content, { message });
diff --git a/packages/preset-hugo/index.js b/packages/preset-hugo/index.js
index 7e0d17e4..45ce435e 100644
--- a/packages/preset-hugo/index.js
+++ b/packages/preset-hugo/index.js
@@ -45,6 +45,24 @@ export default class HugoPreset {
     ];
   }
 
+  /**
+   * Get content
+   * @access private
+   * @param {object} properties - JF2 properties
+   * @returns {string} Content
+   */
+  #content(properties) {
+    if (properties.content) {
+      const content =
+        properties.content.text ||
+        properties.content.html ||
+        properties.content;
+      return `\n${content}\n`;
+    } else {
+      return "";
+    }
+  }
+
   /**
    * Get front matter
    * @access private
@@ -55,6 +73,39 @@ export default class HugoPreset {
     let delimiters;
     let frontMatter;
 
+    /*
+     * Go templates don’t accept hyphens in property names
+     * and Hugo camelCases its predefined front matter keys
+     * @see {link: https://gohugo.io/content-management/front-matter/}
+     */
+    properties = camelcaseKeys(properties, { deep: true });
+
+    /*
+     * Replace Microformat properties with Hugo equivalents
+     * @see {link: https://gohugo.io/content-management/front-matter/}
+     */
+    properties = {
+      date: properties.published,
+      publishDate: properties.published,
+      ...(properties.postStatus === "draft" && { draft: true }),
+      ...(properties.updated && { lastmod: properties.updated }),
+      ...(properties.deleted && { expiryDate: properties.deleted }),
+      ...(properties.name && { title: properties.name }),
+      ...(properties.photo && {
+        images: properties.photo.map((image) => image.url),
+      }),
+      ...properties,
+    };
+
+    delete properties.content; // Shown below front matter
+    delete properties.deleted; // Use `expiryDate`
+    delete properties.name; // Use `title`
+    delete properties.postStatus; // Use `draft`
+    delete properties.published; // Use `date`
+    delete properties.type; // Not required
+    delete properties.updated; // Use `lastmod`
+    delete properties.url; // Not required
+
     switch (this.options.frontMatterFormat) {
       case "json": {
         delimiters = ["", "\n"];
@@ -205,50 +256,7 @@ export default class HugoPreset {
    * @returns {string} Rendered template
    */
   postTemplate(properties) {
-    /*
-     * Go templates don’t accept hyphens in property names
-     * and Hugo camelCases its predefined front matter keys
-     * https://gohugo.io/content-management/front-matter/
-     */
-    properties = camelcaseKeys(properties, { deep: true });
-
-    let content;
-    if (properties.content) {
-      content =
-        properties.content.text ||
-        properties.content.html ||
-        properties.content;
-      content = `\n${content}\n`;
-    } else {
-      content = "";
-    }
-
-    properties = {
-      date: properties.published,
-      publishDate: properties.published,
-      ...(properties.updated && { lastmod: properties.updated }),
-      ...(properties.deleted && { expiryDate: properties.deleted }),
-      ...(properties.name && { title: properties.name }),
-      ...(properties.summary && { summary: properties.summary }),
-      ...(properties.category && { category: properties.category }),
-      ...(properties.start && { start: properties.start }),
-      ...(properties.end && { end: properties.end }),
-      ...(properties.rsvp && { rsvp: properties.rsvp }),
-      ...(properties.location && { location: properties.location }),
-      ...(properties.checkin && { checkin: properties.checkin }),
-      ...(properties.audio && { audio: properties.audio }),
-      ...(properties.photo && { images: properties.photo }),
-      ...(properties.video && { videos: properties.video }),
-      ...(properties.bookmarkOf && { bookmarkOf: properties.bookmarkOf }),
-      ...(properties.likeOf && { likeOf: properties.likeOf }),
-      ...(properties.repostOf && { repostOf: properties.repostOf }),
-      ...(properties.inReplyTo && { inReplyTo: properties.inReplyTo }),
-      ...(properties.postStatus === "draft" && { draft: true }),
-      ...(properties.visibility && { visibility: properties.visibility }),
-      ...(properties.syndication && { syndication: properties.syndication }),
-      ...(properties.references && { references: properties.references }),
-    };
-
+    const content = this.#content(properties);
     const frontMatter = this.#frontMatter(properties);
 
     return frontMatter + content;
diff --git a/packages/preset-jekyll/index.js b/packages/preset-jekyll/index.js
index 5d2b4d0a..15c28eef 100644
--- a/packages/preset-jekyll/index.js
+++ b/packages/preset-jekyll/index.js
@@ -13,6 +13,63 @@ export default class JekyllPreset {
     };
   }
 
+  /**
+   * Get content
+   * @access private
+   * @param {object} properties - JF2 properties
+   * @returns {string} Content
+   */
+  #content(properties) {
+    if (properties.content) {
+      const content =
+        properties.content.text ||
+        properties.content.html ||
+        properties.content;
+      return `\n${content}\n`;
+    } else {
+      return "";
+    }
+  }
+
+  /**
+   * Get front matter
+   * @access private
+   * @param {object} properties - JF2 properties
+   * @returns {string} Front matter in chosen format
+   */
+  #frontMatter(properties) {
+    /*
+     * Replace Microformat properties with Jekyll equivalents
+     * @see {@link https://jekyllrb.com/docs/front-matter/#predefined-variables-for-posts}
+     */
+    properties = {
+      date: properties.published,
+      ...(properties.name && { title: properties.name }),
+      ...(properties.summary && { excerpt: properties.summary }),
+      ...properties,
+    };
+
+    /*
+     * Draft posts
+     * @see {@link https://jekyllrb.com/docs/front-matter/#predefined-global-variables}
+     */
+    if (properties["post-status"] === "draft") {
+      properties.published = false;
+    } else {
+      delete properties.published;
+    }
+
+    delete properties.content; // Shown below front matter
+    delete properties.name; // Use `title`
+    delete properties["post-status"]; // Use `published`
+    delete properties.summary; // Use `excerpt`
+    delete properties.type; // Not required
+    delete properties.url; // Not required
+
+    const frontMatter = YAML.stringify(properties, { lineWidth: 0 });
+    return `---\n${frontMatter}---\n`;
+  }
+
   /**
    * Post types
    * @returns {object} Post types configuration
@@ -136,47 +193,8 @@ export default class JekyllPreset {
    * @returns {string} Rendered template
    */
   postTemplate(properties) {
-    let content;
-    if (properties.content) {
-      content =
-        properties.content.text ||
-        properties.content.html ||
-        properties.content;
-      content = `\n${content}\n`;
-    } else {
-      content = "";
-    }
-
-    properties = {
-      date: properties.published,
-      ...(properties.updated && { updated: properties.updated }),
-      ...(properties.deleted && { deleted: properties.deleted }),
-      ...(properties.name && { title: properties.name }),
-      ...(properties.summary && { excerpt: properties.summary }),
-      ...(properties.category && { category: properties.category }),
-      ...(properties.start && { start: properties.start }),
-      ...(properties.end && { end: properties.end }),
-      ...(properties.rsvp && { rsvp: properties.rsvp }),
-      ...(properties.location && { location: properties.location }),
-      ...(properties.checkin && { checkin: properties.checkin }),
-      ...(properties.audio && { audio: properties.audio }),
-      ...(properties.photo && { photo: properties.photo }),
-      ...(properties.video && { video: properties.video }),
-      ...(properties["bookmark-of"] && {
-        "bookmark-of": properties["bookmark-of"],
-      }),
-      ...(properties["like-of"] && { "like-of": properties["like-of"] }),
-      ...(properties["repost-of"] && { "repost-of": properties["repost-of"] }),
-      ...(properties["in-reply-to"] && {
-        "in-reply-to": properties["in-reply-to"],
-      }),
-      ...(properties["post-status"] === "draft" && { published: false }),
-      ...(properties.visibility && { visibility: properties.visibility }),
-      ...(properties.syndication && { syndication: properties.syndication }),
-      ...(properties.references && { references: properties.references }),
-    };
-    let frontMatter = YAML.stringify(properties, { lineWidth: 0 });
-    frontMatter = `---\n${frontMatter}---\n`;
+    const content = this.#content(properties);
+    const frontMatter = this.#frontMatter(properties);
 
     return frontMatter + content;
   }


Test patch:
diff --git a/packages/preset-hugo/test/index.js b/packages/preset-hugo/test/index.js
index e1897952..74144e66 100644
--- a/packages/preset-hugo/test/index.js
+++ b/packages/preset-hugo/test/index.js
@@ -4,10 +4,10 @@ import { Indiekit } from "@indiekit/indiekit";
 import { getFixture } from "@indiekit-test/fixtures";
 import HugoPreset from "../index.js";
 
-describe("preset-jekyll", () => {
+describe("preset-hugo", () => {
   const hugo = new HugoPreset();
 
-  const properties = JSON.parse(getFixture("jf2/all-properties.jf2"));
+  const properties = JSON.parse(getFixture("jf2/post-template-properties.jf2"));
 
   it("Gets plug-in info", () => {
     assert.equal(hugo.name, "Hugo preset");
@@ -55,12 +55,13 @@ title: What I had for lunch
     );
   });
 
-  it("Renders post template with basic content", () => {
+  it("Renders post template with basic draft content", () => {
     const result = hugo.postTemplate({
       published: "2020-02-02",
       name: "What I had for lunch",
       content:
         "I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.",
+      "post-status": "draft",
     });
 
     assert.equal(
@@ -68,6 +69,7 @@ title: What I had for lunch
       `---
 date: 2020-02-02
 publishDate: 2020-02-02
+draft: true
 title: What I had for lunch
 ---
 
@@ -108,46 +110,48 @@ title: What I had for lunch
   "date": "2020-02-02",
   "publishDate": "2020-02-02",
   "title": "What I had for lunch",
+  "images": [
+    "https://website.example/photo.jpg"
+  ],
   "summary": "A very satisfactory meal.",
   "category": [
     "lunch",
     "food"
   ],
-  "start": "2020-02-02",
-  "end": "2020-02-20",
-  "rsvp": "Yes",
-  "location": {
-    "type": "geo",
-    "latitude": "37.780080",
-    "longitude": "-122.420160",
-    "name": "37° 46′ 48.29″ N 122° 25′ 12.576″ W"
-  },
-  "checkin": {
-    "type": "card",
-    "latitude": "50",
-    "longitude": "0"
-  },
   "audio": [
     {
       "url": "https://website.example/audio.mp3"
     }
   ],
-  "images": [
+  "photo": [
     {
       "alt": "Alternative text",
       "url": "https://website.example/photo.jpg"
     }
   ],
-  "videos": [
+  "video": [
     {
       "url": "https://website.example/video.mp4"
     }
   ],
+  "start": "2020-02-02",
+  "end": "2020-02-20",
+  "rsvp": "Yes",
+  "location": {
+    "type": "geo",
+    "latitude": "37.780080",
+    "longitude": "-122.420160",
+    "name": "37° 46′ 48.29″ N 122° 25′ 12.576″ W"
+  },
+  "checkin": {
+    "type": "card",
+    "latitude": "50",
+    "longitude": "0"
+  },
   "bookmarkOf": "https://website.example",
   "likeOf": "https://website.example",
   "repostOf": "https://website.example",
   "inReplyTo": "https://website.example",
-  "draft": true,
   "visibility": "private",
   "syndication": "https://website.example/post/12345"
 }
@@ -167,6 +171,7 @@ I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.
 date = "2020-02-02"
 publishDate = "2020-02-02"
 title = "What I had for lunch"
+images = [ "https://website.example/photo.jpg" ]
 summary = "A very satisfactory meal."
 category = [ "lunch", "food" ]
 start = "2020-02-02"
@@ -176,10 +181,19 @@ bookmarkOf = "https://website.example"
 likeOf = "https://website.example"
 repostOf = "https://website.example"
 inReplyTo = "https://website.example"
-draft = true
 visibility = "private"
 syndication = "https://website.example/post/12345"
 
+[[audio]]
+url = "https://website.example/audio.mp3"
+
+[[photo]]
+alt = "Alternative text"
+url = "https://website.example/photo.jpg"
+
+[[video]]
+url = "https://website.example/video.mp4"
+
 [location]
 type = "geo"
 latitude = "37.780080"
@@ -190,16 +204,6 @@ name = "37° 46′ 48.29″ N 122° 25′ 12.576″ W"
 type = "card"
 latitude = "50"
 longitude = "0"
-
-[[audio]]
-url = "https://website.example/audio.mp3"
-
-[[images]]
-alt = "Alternative text"
-url = "https://website.example/photo.jpg"
-
-[[videos]]
-url = "https://website.example/video.mp4"
 +++
 
 I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.
@@ -217,10 +221,19 @@ I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.
 date: 2020-02-02
 publishDate: 2020-02-02
 title: What I had for lunch
+images:
+  - https://website.example/photo.jpg
 summary: A very satisfactory meal.
 category:
   - lunch
   - food
+audio:
+  - url: https://website.example/audio.mp3
+photo:
+  - alt: Alternative text
+    url: https://website.example/photo.jpg
+video:
+  - url: https://website.example/video.mp4
 start: 2020-02-02
 end: 2020-02-20
 rsvp: Yes
@@ -233,18 +246,10 @@ checkin:
   type: card
   latitude: "50"
   longitude: "0"
-audio:
-  - url: https://website.example/audio.mp3
-images:
-  - alt: Alternative text
-    url: https://website.example/photo.jpg
-videos:
-  - url: https://website.example/video.mp4
 bookmarkOf: https://website.example
 likeOf: https://website.example
 repostOf: https://website.example
 inReplyTo: https://website.example
-draft: true
 visibility: private
 syndication: https://website.example/post/12345
 ---
diff --git a/packages/preset-jekyll/test/index.js b/packages/preset-jekyll/test/index.js
index cb0a235c..f200411a 100644
--- a/packages/preset-jekyll/test/index.js
+++ b/packages/preset-jekyll/test/index.js
@@ -7,7 +7,7 @@ import JekyllPreset from "../index.js";
 describe("preset-jekyll", () => {
   const jekyll = new JekyllPreset();
 
-  const properties = JSON.parse(getFixture("jf2/all-properties.jf2"));
+  const properties = JSON.parse(getFixture("jf2/post-template-properties.jf2"));
 
   it("Gets plug-in info", () => {
     assert.equal(jekyll.name, "Jekyll preset");
@@ -39,20 +39,21 @@ describe("preset-jekyll", () => {
       result,
       `---
 date: 2020-02-02
+title: Lunchtime
 updated: 2022-12-11
 deleted: 2022-12-12
-title: Lunchtime
 ---
 `,
     );
   });
 
-  it("Renders post template with basic content", () => {
+  it("Renders post template with basic draft content", () => {
     const result = jekyll.postTemplate({
       published: "2020-02-02",
       name: "Lunchtime",
       content:
         "I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.",
+      "post-status": "draft",
     });
 
     assert.equal(
@@ -60,6 +61,7 @@ title: Lunchtime
       `---
 date: 2020-02-02
 title: Lunchtime
+published: false
 ---
 
 I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.
@@ -101,6 +103,13 @@ excerpt: A very satisfactory meal.
 category:
   - lunch
   - food
+audio:
+  - url: https://website.example/audio.mp3
+photo:
+  - alt: Alternative text
+    url: https://website.example/photo.jpg
+video:
+  - url: https://website.example/video.mp4
 start: 2020-02-02
 end: 2020-02-20
 rsvp: Yes
@@ -113,18 +122,10 @@ checkin:
   type: card
   latitude: "50"
   longitude: "0"
-audio:
-  - url: https://website.example/audio.mp3
-photo:
-  - alt: Alternative text
-    url: https://website.example/photo.jpg
-video:
-  - url: https://website.example/video.mp4
 bookmark-of: https://website.example
 like-of: https://website.example
 repost-of: https://website.example
 in-reply-to: https://website.example
-published: false
 visibility: private
 syndication: https://website.example/post/12345
 ---