Repository: getindiekit/indiekit
Pull request #706 (2024-03-10 12:19:59)

Add 11ty publication preset plugin


Patch:
diff --git a/docs/plugins/presets.md b/docs/plugins/presets.md
index b942d01f..ce119068 100644
--- a/docs/plugins/presets.md
+++ b/docs/plugins/presets.md
@@ -2,6 +2,12 @@
 
 A [publication preset](../concepts#publication-preset) provides default values for post types and post templates. Plug-ins are available for the following platforms:
 
+## [@indiekit/preset-eleventy](https://npmjs.org/package/@indiekit/preset-eleventy)
+
+<Badge type="tip" text="Offical" />
+
+[Eleventy](https://www.11ty.dev) publication preset.
+
 ## [@indiekit/preset-hugo](https://npmjs.org/package/@indiekit/preset-hugo)
 
 <Badge type="tip" text="Offical" />
diff --git a/indiekit.config.js b/indiekit.config.js
index f52f728d..26e27e96 100644
--- a/indiekit.config.js
+++ b/indiekit.config.js
@@ -21,7 +21,7 @@ const config = {
     "@indiekit/post-type-repost",
     "@indiekit/post-type-rsvp",
     "@indiekit/post-type-video",
-    "@indiekit/preset-jekyll",
+    "@indiekit/preset-eleventy",
     "@indiekit/store-github",
     "@indiekit/syndicator-internet-archive",
     "@indiekit/syndicator-mastodon",
diff --git a/package-lock.json b/package-lock.json
index 5b23262f..a8ae7870 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2706,6 +2706,10 @@
       "resolved": "packages/post-type-video",
       "link": true
     },
+    "node_modules/@indiekit/preset-eleventy": {
+      "resolved": "packages/preset-eleventy",
+      "link": true
+    },
     "node_modules/@indiekit/preset-hugo": {
       "resolved": "packages/preset-hugo",
       "link": true
@@ -20623,6 +20627,90 @@
         "node": ">=20"
       }
     },
+    "packages/preset-eleventy": {
+      "version": "1.0.0-beta.8",
+      "license": "MIT",
+      "dependencies": {
+        "camelcase-keys": "^9.0.0",
+        "plur": "^5.1.0",
+        "yaml": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "packages/preset-eleventy/node_modules/camelcase": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
+      "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "packages/preset-eleventy/node_modules/camelcase-keys": {
+      "version": "9.1.3",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz",
+      "integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==",
+      "dependencies": {
+        "camelcase": "^8.0.0",
+        "map-obj": "5.0.0",
+        "quick-lru": "^6.1.1",
+        "type-fest": "^4.3.2"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "packages/preset-eleventy/node_modules/map-obj": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz",
+      "integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "packages/preset-eleventy/node_modules/quick-lru": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
+      "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "packages/preset-eleventy/node_modules/type-fest": {
+      "version": "4.11.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.11.1.tgz",
+      "integrity": "sha512-MFMf6VkEVZAETidGGSYW2B1MjXbGX+sWIywn2QPEaJ3j08V+MwVRHMXtf2noB8ENJaD0LIun9wh5Z6OPNf1QzQ==",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "packages/preset-eleventy/node_modules/yaml": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
+      "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "packages/preset-hugo": {
       "name": "@indiekit/preset-hugo",
       "version": "1.0.0-beta.8",
diff --git a/packages/preset-eleventy/README.md b/packages/preset-eleventy/README.md
new file mode 100644
index 00000000..eb59888c
--- /dev/null
+++ b/packages/preset-eleventy/README.md
@@ -0,0 +1,17 @@
+# @indiekit/preset-eleventy
+
+[Eleventy](https://www.11ty.dev) publication preset for Indiekit.
+
+## Installation
+
+`npm i @indiekit/preset-eleventy`
+
+## Usage
+
+Add `@indiekit/preset-eleventy` to your list of plug-ins:
+
+```json
+{
+  "plugins": ["@indiekit/preset-eleventy"]
+}
+```
diff --git a/packages/preset-eleventy/assets/icon.svg b/packages/preset-eleventy/assets/icon.svg
new file mode 100644
index 00000000..b60b8c61
--- /dev/null
+++ b/packages/preset-eleventy/assets/icon.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
+  <path fill="#222" d="M0 0h96v96H0z"/>
+  <path fill="#FFF" d="M42.7 27c1 0 1.3.8 1.3 2v35.5c0 1.6-.4 2.5-1.4 2.5h-4.1c-1 0-1.4-1-1.4-2.5V34.8h-.2l-.8.2-.5.2h-.2c-1 .1-1.4-.7-1.4-1.9v-2.5c0-1 .4-1.8 1.2-2l7-1.7.5-.1Zm11.9-1c1 0 1.4.8 1.4 2.1v10.4h3c1 0 1.5 1 1.5 2V42c0 1.2-.5 2-1.5 2h-3v13.7c0 .9 0 1.6.2 2.2v.3l.4 1.2.6.4.7.1h1.9c1 0 1.5.7 1.5 1.7v1.6c0 1-.6 1.8-1.6 1.8H57c-.8 0-1.5 0-2-.2-.7 0-1.4-.3-2-.7a5 5 0 0 1-1.7-1.7c-.4-.7-.7-1.6-1-2.8-.2-1.1-.4-2.5-.4-4V44h-1.4c-1 0-1.5-.7-1.5-1.8v-1.6c0-1.2.5-2.1 1.5-2.1H50l.4-10.4c.1-1.3.5-2.1 1.5-2.1h2.7Zm22.2 13c.9 0 1.2.7 1.2 1.8v.7l-4.8 25.2a26 26 0 0 1-1.3 5c-.5 1.2-1 2-1.7 2.5s-1.5.8-2.6.8h-.3c-1.6 0-2.8-.5-3.5-1.5-.2-.3-.3-.8-.3-1.4v-.2c0-1.8.3-2.8 1.3-2.8l1.2.4 1 .2s.2-.1.4-.6c.3-.6.4-1.5.4-2.6l-.5-2.9L62 41.4l-.1-.7c0-1 .3-1.7 1.1-1.7h3.2c.5 0 .8.2 1.1.5.3.3.5.7.6 1.4l2.8 15.3L73 41c0-.6.2-1 .4-1.3v-.1c.4-.4.8-.6 1.2-.6h2.2ZM28.7 27c1 0 1.3.8 1.3 2v35.5c0 1.6-.4 2.5-1.4 2.5h-4.1c-1 0-1.4-1-1.4-2.5V34.8h-.2l-.8.2-.5.2h-.2c-1 .1-1.4-.7-1.4-1.9v-2.5c0-1 .4-1.8 1.2-2l7-1.7.5-.1Z"/>
+</svg>
diff --git a/packages/preset-eleventy/index.js b/packages/preset-eleventy/index.js
new file mode 100644
index 00000000..b46b4ace
--- /dev/null
+++ b/packages/preset-eleventy/index.js
@@ -0,0 +1,25 @@
+import { getPostTemplate } from "./lib/post-template.js";
+import { getPostTypes } from "./lib/post-types.js";
+
+export default class EleventyPreset {
+  constructor() {
+    this.name = "Eleventy preset";
+  }
+
+  get info() {
+    return {
+      name: "Eleventy",
+    };
+  }
+
+  postTemplate(properties) {
+    return getPostTemplate(properties);
+  }
+
+  init(Indiekit) {
+    const { application } = Indiekit.config;
+    this.postTypes = getPostTypes(application.postTypes);
+
+    Indiekit.addPreset(this);
+  }
+}
diff --git a/packages/preset-eleventy/lib/post-template.js b/packages/preset-eleventy/lib/post-template.js
new file mode 100644
index 00000000..7c21351f
--- /dev/null
+++ b/packages/preset-eleventy/lib/post-template.js
@@ -0,0 +1,73 @@
+import camelcaseKeys from "camelcase-keys";
+import YAML from "yaml";
+
+/**
+ * Get content
+ * @access private
+ * @param {object} properties - JF2 properties
+ * @returns {string} Content
+ */
+const getContent = (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
+ */
+const getFrontMatter = (properties) => {
+  /**
+   * Eleventy uses camelCase for YAML property keys, i.e. `fileSlug`
+   * @see {@link https://www.11ty.dev/docs/data-eleventy-supplied/}
+   */
+  properties = camelcaseKeys(properties, { deep: true });
+
+  /**
+   * Replace Microformat properties with Eleventy equivalents
+   * @see {@link https://www.11ty.dev/docs/data-frontmatter/}
+   * @see {@link https://www.11ty.dev/docs/dates/}
+   */
+  properties = {
+    date: properties.published,
+    ...(properties.name && { title: properties.name }),
+    ...properties,
+  };
+
+  /**
+   * Draft posts
+   * @see {@link https://www.11ty.dev/docs/quicktips/draft-posts/}
+   */
+  if (properties.postStatus === "draft") {
+    properties.draft = true;
+  }
+
+  delete properties.content; // Shown below front matter
+  delete properties.name; // Use `title`
+  delete properties.postStatus; // Use `draft`
+  delete properties.published; // Use `date`
+  delete properties.type; // Not required
+  delete properties.url; // Not required
+
+  const frontMatter = YAML.stringify(properties, { lineWidth: 0 });
+  return `---\n${frontMatter}---\n`;
+};
+
+/**
+ * Get post template
+ * @param {object} properties - JF2 properties
+ * @returns {string} Rendered template
+ */
+export const getPostTemplate = (properties) => {
+  const content = getContent(properties);
+  const frontMatter = getFrontMatter(properties);
+
+  return frontMatter + content;
+};
diff --git a/packages/preset-eleventy/lib/post-types.js b/packages/preset-eleventy/lib/post-types.js
new file mode 100644
index 00000000..c3234690
--- /dev/null
+++ b/packages/preset-eleventy/lib/post-types.js
@@ -0,0 +1,25 @@
+import plur from "plur";
+
+/**
+ * Get paths and URLs for configured post types
+ * @param {object} postTypes - Post type configuration
+ * @returns {object} Updated post type configuration
+ */
+export const getPostTypes = (postTypes) => {
+  for (const type of Object.keys(postTypes)) {
+    const collection = plur(type);
+
+    postTypes[type] = {
+      ...postTypes[type],
+      post: {
+        path: `${collection}/{yyyy}-{MM}-{dd}-{slug}.md`,
+        url: `${collection}/{yyyy}/{MM}/{dd}/{slug}`,
+      },
+      media: {
+        path: `media/${collection}/{yyyy}/{MM}/{dd}/{filename}`,
+      },
+    };
+  }
+
+  return postTypes;
+};
diff --git a/packages/preset-eleventy/package.json b/packages/preset-eleventy/package.json
new file mode 100644
index 00000000..d332d518
--- /dev/null
+++ b/packages/preset-eleventy/package.json
@@ -0,0 +1,45 @@
+{
+  "name": "@indiekit/preset-eleventy",
+  "version": "1.0.0-beta.8",
+  "description": "Eleventy publication preset for Indiekit",
+  "keywords": [
+    "indiekit",
+    "indiekit-plugin",
+    "indieweb",
+    "micropub",
+    "eleventy",
+    "11ty"
+  ],
+  "homepage": "https://getindiekit.com",
+  "author": {
+    "name": "Paul Robert Lloyd",
+    "url": "https://paulrobertlloyd.com"
+  },
+  "license": "MIT",
+  "engines": {
+    "node": ">=20"
+  },
+  "type": "module",
+  "main": "index.js",
+  "files": [
+    "assets",
+    "lib",
+    "index.js"
+  ],
+  "bugs": {
+    "url": "https://github.com/getindiekit/indiekit/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/getindiekit/indiekit.git",
+    "directory": "packages/preset-eleventy"
+  },
+  "dependencies": {
+    "plur": "^5.1.0",
+    "camelcase-keys": "^9.0.0",
+    "yaml": "^2.4.0"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}


Test patch:
diff --git a/packages/preset-eleventy/test/index.js b/packages/preset-eleventy/test/index.js
new file mode 100644
index 00000000..e058bdea
--- /dev/null
+++ b/packages/preset-eleventy/test/index.js
@@ -0,0 +1,48 @@
+import { strict as assert } from "node:assert";
+import { describe, it } from "node:test";
+import { Indiekit } from "@indiekit/indiekit";
+import EleventyPreset from "../index.js";
+
+describe("preset-eleventy", async () => {
+  const eleventy = new EleventyPreset();
+  const indiekit = await Indiekit.initialize({
+    config: {
+      publication: {
+        me: "https://website.example",
+        postTypes: {
+          puppy: {
+            name: "Puppy posts",
+          },
+        },
+      },
+    },
+  });
+  const bootstrappedConfig = await indiekit.bootstrap();
+
+  eleventy.init(bootstrappedConfig);
+
+  it("Initiates plug-in", async () => {
+    assert.equal(indiekit.publication.preset.info.name, "Eleventy");
+  });
+
+  it("Gets plug-in info", () => {
+    assert.equal(eleventy.name, "Eleventy preset");
+    assert.equal(eleventy.info.name, "Eleventy");
+  });
+
+  it("Gets publication post types", () => {
+    assert.deepEqual(eleventy.postTypes.article.post, {
+      path: "articles/{yyyy}-{MM}-{dd}-{slug}.md",
+      url: "articles/{yyyy}/{MM}/{dd}/{slug}",
+    });
+  });
+
+  it("Renders post template", () => {
+    const result = eleventy.postTemplate({
+      published: "2020-02-02",
+      name: "Lunchtime",
+    });
+
+    assert.equal(result.includes("date: 2020-02-02"), true);
+  });
+});
diff --git a/packages/preset-eleventy/test/unit/post-template.js b/packages/preset-eleventy/test/unit/post-template.js
new file mode 100644
index 00000000..6208d9ab
--- /dev/null
+++ b/packages/preset-eleventy/test/unit/post-template.js
@@ -0,0 +1,111 @@
+import { strict as assert } from "node:assert";
+import { describe, it } from "node:test";
+import { getFixture } from "@indiekit-test/fixtures";
+import { getPostTemplate } from "../../lib/post-template.js";
+
+describe("preset-jekyll/lib/post-template", async () => {
+  const properties = JSON.parse(getFixture("jf2/post-template-properties.jf2"));
+
+  it("Renders post template without content", () => {
+    const result = getPostTemplate({
+      published: "2020-02-02",
+      updated: "2022-12-11",
+      deleted: "2022-12-12",
+      name: "Lunchtime",
+    });
+
+    assert.equal(
+      result,
+      `---
+date: 2020-02-02
+title: Lunchtime
+updated: 2022-12-11
+deleted: 2022-12-12
+---
+`,
+    );
+  });
+
+  it("Renders post template with basic draft content", () => {
+    const result = getPostTemplate({
+      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(
+      result,
+      `---
+date: 2020-02-02
+title: Lunchtime
+draft: true
+---
+
+I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.
+`,
+    );
+  });
+
+  it("Renders post template with HTML content", () => {
+    const result = getPostTemplate({
+      published: "2020-02-02",
+      name: "Lunchtime",
+      content: {
+        html: '<p>I ate a <a href="https://en.wikipedia.org/wiki/Cheese">cheese</a> sandwich, which was nice.</p>',
+      },
+    });
+
+    assert.equal(
+      result,
+      `---
+date: 2020-02-02
+title: Lunchtime
+---
+
+<p>I ate a <a href="https://en.wikipedia.org/wiki/Cheese">cheese</a> sandwich, which was nice.</p>
+`,
+    );
+  });
+
+  it("Renders post template", () => {
+    const result = getPostTemplate(properties);
+
+    assert.equal(
+      result,
+      `---
+date: 2020-02-02
+title: What I had for lunch
+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
+location:
+  type: geo
+  latitude: "37.780080"
+  longitude: "-122.420160"
+  name: 37° 46′ 48.29″ N 122° 25′ 12.576″ W
+bookmarkOf: https://website.example
+likeOf: https://website.example
+repostOf: https://website.example
+inReplyTo: https://website.example
+visibility: private
+syndication: https://website.example/post/12345
+---
+
+I ate a [cheese](https://en.wikipedia.org/wiki/Cheese) sandwich, which was nice.
+`,
+    );
+  });
+});
diff --git a/packages/preset-eleventy/test/unit/post-types.js b/packages/preset-eleventy/test/unit/post-types.js
new file mode 100644
index 00000000..6c892b72
--- /dev/null
+++ b/packages/preset-eleventy/test/unit/post-types.js
@@ -0,0 +1,52 @@
+import { strict as assert } from "node:assert";
+import { describe, it } from "node:test";
+import { getPostTypes } from "../../lib/post-types.js";
+
+const postTypes = {
+  article: {
+    name: "Journal post",
+  },
+  note: {
+    name: "Micro post",
+  },
+  puppy: {
+    name: "Puppy post",
+  },
+};
+
+describe("preset-jekyll/lib/post-types", () => {
+  it("Gets paths and URLs for configured post types", () => {
+    assert.deepEqual(getPostTypes(postTypes), {
+      article: {
+        name: "Journal post",
+        post: {
+          path: "articles/{yyyy}-{MM}-{dd}-{slug}.md",
+          url: "articles/{yyyy}/{MM}/{dd}/{slug}",
+        },
+        media: {
+          path: "media/articles/{yyyy}/{MM}/{dd}/{filename}",
+        },
+      },
+      note: {
+        name: "Micro post",
+        post: {
+          path: "notes/{yyyy}-{MM}-{dd}-{slug}.md",
+          url: "notes/{yyyy}/{MM}/{dd}/{slug}",
+        },
+        media: {
+          path: "media/notes/{yyyy}/{MM}/{dd}/{filename}",
+        },
+      },
+      puppy: {
+        name: "Puppy post",
+        post: {
+          path: "puppies/{yyyy}-{MM}-{dd}-{slug}.md",
+          url: "puppies/{yyyy}/{MM}/{dd}/{slug}",
+        },
+        media: {
+          path: "media/puppies/{yyyy}/{MM}/{dd}/{filename}",
+        },
+      },
+    });
+  });
+});