Repository: getindiekit/indiekit
Pull request #687 (2024-01-29 22:45:23)

Store post type config in object
I was right the first time…

Previously, post type configuration was stored using keyed values for each post type, i.e.:

```js
{
  article: {
    name: "Article"
  }
}
```

I later changed this to use an array of objects with a `type` property, i.e.:

```js
[
  {
    type: "article",
    name: "Article"
  }
}
```

This was to align with the expected format when querying a Micropub endpoint. And yet, when it comes to managing, transforming and merging post types, it's easier to merge based on a key. And when you think about it, there shouldn’t be multiple objects with the same `type` value, and with the former format we can infer this.

As part of a broader change as to how post types are configured and customised, this PR reverts back to former model. Meanwhile, the model expected to be provided in by a Micropub config query is retained.

(I’ll update the documentation when the follow-on changes are merged)


Patch:
diff --git a/helpers/config/index.js b/helpers/config/index.js
index 6f446d4a..44e45c75 100644
--- a/helpers/config/index.js
+++ b/helpers/config/index.js
@@ -20,9 +20,8 @@ export const testConfig = async (options) => {
   }
 
   // Configure custom note post type with date-less URL for easier testing
-  const postTypes = [
-    {
-      type: "note",
+  const postTypes = {
+    note: {
       name: "Custom note post type",
       fields: ["content", "geo"],
       post: {
@@ -30,8 +29,7 @@ export const testConfig = async (options) => {
         url: "notes/{slug}/",
       },
     },
-    {
-      type: "photo",
+    photo: {
       name: "Custom photo post type",
       fields: ["photo"],
       post: {
@@ -43,7 +41,7 @@ export const testConfig = async (options) => {
         url: "media/photos/{filename}",
       },
     },
-  ];
+  };
 
   return {
     application: {
diff --git a/indiekit.config.js b/indiekit.config.js
index 54bf5caf..db174b6f 100644
--- a/indiekit.config.js
+++ b/indiekit.config.js
@@ -24,15 +24,13 @@ const config = {
     me: process.env.PUBLICATION_URL,
     categories: ["internet", "indieweb", "indiekit", "test", "testing"],
     enrichPostData: true,
-    postTypes: [
-      {
-        type: "like",
+    postTypes: {
+      like: {
         name: "Favourite",
         fields: ["like-of", "published"],
         requiredFields: ["like-of", "published"],
       },
-      {
-        type: "jam",
+      jam: {
         name: "Jam",
         discovery: "jam-of",
         h: "entry",
@@ -43,7 +41,7 @@ const config = {
           url: "jams/{yyyy}/{MM}/{dd}/{slug}",
         },
       },
-    ],
+    },
   },
   "@indiekit/store-github": {
     user: process.env.GITHUB_USER,
diff --git a/packages/endpoint-media/lib/media-data.js b/packages/endpoint-media/lib/media-data.js
index 59d03c07..e8b35bd4 100644
--- a/packages/endpoint-media/lib/media-data.js
+++ b/packages/endpoint-media/lib/media-data.js
@@ -1,7 +1,7 @@
 import { IndiekitError } from "@indiekit/error";
 import { getCanonicalUrl } from "@indiekit/util";
-import { getPostTypeConfig, renderPath } from "./utils.js";
 import { getFileProperties, getMediaType } from "./file.js";
+import { renderPath } from "./utils.js";
 
 export const mediaData = {
   /**
@@ -29,7 +29,7 @@ export const mediaData = {
     }
 
     // Get post type configuration
-    const typeConfig = getPostTypeConfig(type, postTypes);
+    const typeConfig = postTypes[type];
     if (!typeConfig) {
       throw IndiekitError.notImplemented(type);
     }
diff --git a/packages/endpoint-media/lib/utils.js b/packages/endpoint-media/lib/utils.js
index 0c291ea7..158d005e 100644
--- a/packages/endpoint-media/lib/utils.js
+++ b/packages/endpoint-media/lib/utils.js
@@ -87,12 +87,3 @@ export const renderPath = async (path, properties, application) => {
 
   return path;
 };
-
-/**
- * Get post type configuration for a given type
- * @param {string} type - Post type
- * @param {object} postTypes - Publication post types
- * @returns {object} Post type configuration
- */
-export const getPostTypeConfig = (type, postTypes) =>
-  postTypes.find((item) => item.type === type);
diff --git a/packages/endpoint-micropub/lib/config.js b/packages/endpoint-micropub/lib/config.js
index 8b25fe0d..cd591ad5 100644
--- a/packages/endpoint-micropub/lib/config.js
+++ b/packages/endpoint-micropub/lib/config.js
@@ -29,7 +29,7 @@ export const getConfig = (application, publication) => {
   return {
     categories,
     "media-endpoint": mediaEndpoint,
-    "post-types": postTypes.map((postType) => ({
+    "post-types": Object.values(postTypes).map((postType) => ({
       type: postType.type,
       name: postType.name,
       h: postType.h,
diff --git a/packages/endpoint-micropub/lib/post-data.js b/packages/endpoint-micropub/lib/post-data.js
index dd3cab81..1f1d64af 100644
--- a/packages/endpoint-micropub/lib/post-data.js
+++ b/packages/endpoint-micropub/lib/post-data.js
@@ -4,7 +4,7 @@ import { getCanonicalUrl, getDate } from "@indiekit/util";
 import { getPostType } from "./post-type-discovery.js";
 import { getSyndicateToProperty, normaliseProperties } from "./jf2.js";
 import * as updateMf2 from "./update.js";
-import { getPostTypeConfig, renderPath } from "./utils.js";
+import { renderPath } from "./utils.js";
 
 export const postData = {
   /**
@@ -33,7 +33,7 @@ export const postData = {
     properties["post-type"] = type;
 
     // Get post type configuration
-    const typeConfig = getPostTypeConfig(type, postTypes);
+    const typeConfig = postTypes[type];
     if (!typeConfig) {
       throw IndiekitError.notImplemented(type);
     }
@@ -126,7 +126,7 @@ export const postData = {
 
     // Post type
     const type = getPostType(postTypes, properties);
-    const typeConfig = getPostTypeConfig(type, postTypes);
+    const typeConfig = postTypes[type];
     properties["post-type"] = type;
 
     // Post paths
@@ -188,7 +188,8 @@ export const postData = {
     properties.deleted = getDate(timeZone);
 
     // Post type
-    const typeConfig = getPostTypeConfig(properties["post-type"], postTypes);
+    const type = properties["post-type"];
+    const typeConfig = postTypes[type];
 
     // Post paths
     const path = await renderPath(
@@ -226,7 +227,8 @@ export const postData = {
     const properties = _deletedProperties;
 
     // Post type
-    const typeConfig = getPostTypeConfig(properties["post-type"], postTypes);
+    const type = properties["post-type"];
+    const typeConfig = postTypes[type];
 
     // Post paths
     const path = await renderPath(
diff --git a/packages/endpoint-micropub/lib/post-type-discovery.js b/packages/endpoint-micropub/lib/post-type-discovery.js
index cedb4ce5..cb03819e 100644
--- a/packages/endpoint-micropub/lib/post-type-discovery.js
+++ b/packages/endpoint-micropub/lib/post-type-discovery.js
@@ -24,8 +24,12 @@ export const getPostType = (postTypes, properties) => {
   basePostTypes.set("photo", "photo");
 
   // Types defined in post type configuration
-  for (const postType of postTypes) {
-    basePostTypes.set(postType.type, postType.discovery);
+  for (const [type, { discovery }] of Object.entries(postTypes)) {
+    if (!discovery) {
+      continue;
+    }
+
+    basePostTypes.set(type, discovery);
   }
 
   for (const basePostType of basePostTypes) {
diff --git a/packages/endpoint-micropub/lib/utils.js b/packages/endpoint-micropub/lib/utils.js
index 3bb3d223..71f4e6ef 100644
--- a/packages/endpoint-micropub/lib/utils.js
+++ b/packages/endpoint-micropub/lib/utils.js
@@ -27,15 +27,6 @@ export const excerptString = (string, n) => {
   }
 };
 
-/**
- * Get post type configuration for a given type
- * @param {string} type - Post type
- * @param {object} postTypes - Publication post types
- * @returns {object} Post type configuration
- */
-export const getPostTypeConfig = (type, postTypes) =>
-  postTypes.find((item) => item.type === type);
-
 /**
  * Get post template properties
  * @param {object} properties - JF2 properties
diff --git a/packages/endpoint-posts/includes/endpoint-posts-widget.njk b/packages/endpoint-posts/includes/endpoint-posts-widget.njk
index 79fc673f..a5b98206 100644
--- a/packages/endpoint-posts/includes/endpoint-posts-widget.njk
+++ b/packages/endpoint-posts/includes/endpoint-posts-widget.njk
@@ -3,7 +3,7 @@
   image: "/assets/" + plugin.id + "/icon.svg"
 }) %}
 {% if application.hasDatabase %}
-  <div class="button-grid">{% for config in publication.postTypes %}
+  <div class="button-grid">{% for type, config in publication.postTypes | dictsort %}
     {{ button({
       classes: "button--secondary-on-offset button--inline",
       href: application.postsEndpoint + "/create/?type=" + config.type,
diff --git a/packages/endpoint-posts/lib/controllers/new.js b/packages/endpoint-posts/lib/controllers/new.js
index 25aad23a..b0c7bea0 100644
--- a/packages/endpoint-posts/lib/controllers/new.js
+++ b/packages/endpoint-posts/lib/controllers/new.js
@@ -14,7 +14,7 @@ export const newController = {
     const postType = request.query.type;
     const { scope } = request.session;
 
-    const postTypeItems = publication.postTypes
+    const postTypeItems = Object.values(publication.postTypes)
       .sort((a, b) => {
         return a.name.localeCompare(b.name);
       })
@@ -51,7 +51,8 @@ export const newController = {
   async post(request, response) {
     const { publication } = request.app.locals;
     const postsPath = path.dirname(request.baseUrl + request.path);
-    const postTypeItems = publication.postTypes
+
+    const postTypeItems = Object.values(publication.postTypes)
       .sort((a, b) => {
         return a.name.localeCompare(b.name);
       })
diff --git a/packages/endpoint-posts/lib/middleware/post-data.js b/packages/endpoint-posts/lib/middleware/post-data.js
index ed4a85f4..6597401e 100644
--- a/packages/endpoint-posts/lib/middleware/post-data.js
+++ b/packages/endpoint-posts/lib/middleware/post-data.js
@@ -5,7 +5,6 @@ import {
   getGeoValue,
   getPostName,
   getPostProperties,
-  getPostTypeConfig,
   getSyndicateToItems,
 } from "../utils.js";
 
@@ -19,10 +18,7 @@ export const postData = {
     const properties = request.body;
 
     // Get post type config
-    const { name, fields, h, requiredFields } = getPostTypeConfig(
-      publication,
-      postType,
-    );
+    const { name, fields, h, requiredFields } = publication.postTypes[postType];
 
     // Only select ‘checked’ syndication targets on first view
     const checkTargets = Object.entries(request.body).length === 0;
@@ -71,10 +67,8 @@ export const postData = {
       const postType = properties["post-type"];
 
       // Get post type config
-      const { name, fields, h, requiredFields } = getPostTypeConfig(
-        publication,
-        postType,
-      );
+      const { name, fields, h, requiredFields } =
+        publication.postTypes[postType];
 
       response.locals = {
         accessToken: access_token,
diff --git a/packages/endpoint-posts/lib/middleware/validation.js b/packages/endpoint-posts/lib/middleware/validation.js
index 695868dc..36d3d8e3 100644
--- a/packages/endpoint-posts/lib/middleware/validation.js
+++ b/packages/endpoint-posts/lib/middleware/validation.js
@@ -5,12 +5,12 @@ export const validate = {
     const { postTypes } = request.app.locals.publication;
     const validations = [];
 
-    for (const postType of postTypes) {
-      if (!postType.validationSchema) {
+    for (const typeConfig of Object.values(postTypes)) {
+      if (!typeConfig.validationSchema) {
         continue;
       }
 
-      for (const schema of postType.validationSchema) {
+      for (const schema of typeConfig.validationSchema) {
         validations.push(checkSchema(schema));
       }
     }
diff --git a/packages/endpoint-posts/lib/utils.js b/packages/endpoint-posts/lib/utils.js
index 92022fdb..a1cc201c 100644
--- a/packages/endpoint-posts/lib/utils.js
+++ b/packages/endpoint-posts/lib/utils.js
@@ -104,7 +104,8 @@ export const getPostName = (publication, properties) => {
     return properties.name;
   }
 
-  const { name } = getPostTypeConfig(publication, properties["post-type"]);
+  const type = properties["post-type"];
+  const { name } = publication.postTypes[type];
 
   return name;
 };
@@ -131,24 +132,6 @@ export const getPostProperties = async (uid, micropubEndpoint, accessToken) => {
   return false;
 };
 
-/**
- * Get post type config
- * @param {object} publication - Publication configuration
- * @param {string} postType - Post type
- * @returns {object} Post type configuration
- */
-export const getPostTypeConfig = (publication, postType) => {
-  if (publication.postTypes && postType) {
-    const postTypeConfig = publication.postTypes.find(
-      (item) => item.type === postType,
-    );
-
-    return postTypeConfig;
-  }
-
-  return "";
-};
-
 /**
  * Get post URL from ID
  * @param {string} id - ID
diff --git a/packages/indiekit/config/defaults.js b/packages/indiekit/config/defaults.js
index c674238c..3a1be8ac 100644
--- a/packages/indiekit/config/defaults.js
+++ b/packages/indiekit/config/defaults.js
@@ -49,9 +49,8 @@ export const defaultConfig = {
     locale: "en",
     me: undefined,
     postTemplate: undefined,
-    postTypes: [
-      {
-        type: "article",
+    postTypes: {
+      article: {
         name: "Article",
         h: "entry",
         fields: [
@@ -73,8 +72,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "note",
+      note: {
         name: "Note",
         h: "entry",
         fields: [
@@ -93,8 +91,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "photo",
+      photo: {
         name: "Photo",
         h: "entry",
         fields: [
@@ -115,8 +112,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "video",
+      video: {
         name: "Video",
         h: "entry",
         fields: [
@@ -136,8 +132,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "audio",
+      audio: {
         name: "Audio",
         discovery: "audio",
         h: "entry",
@@ -158,8 +153,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "bookmark",
+      bookmark: {
         name: "Bookmark",
         discovery: "bookmark-of",
         h: "entry",
@@ -179,8 +173,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "event",
+      event: {
         name: "Event",
         h: "event",
         fields: [
@@ -203,8 +196,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "rsvp",
+      rsvp: {
         name: "RSVP",
         h: "entry",
         fields: [
@@ -224,8 +216,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "reply",
+      reply: {
         name: "Reply",
         h: "entry",
         fields: [
@@ -244,8 +235,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "repost",
+      repost: {
         name: "Repost",
         h: "entry",
         fields: [
@@ -263,8 +253,7 @@ export const defaultConfig = {
           },
         ],
       },
-      {
-        type: "like",
+      like: {
         name: "Like",
         h: "entry",
         fields: [
@@ -282,7 +271,7 @@ export const defaultConfig = {
           },
         ],
       },
-    ],
+    },
     preset: undefined,
     slugSeparator: "-",
     storeMessageTemplate: (metaData) =>
diff --git a/packages/indiekit/lib/post-types.js b/packages/indiekit/lib/post-types.js
index 9d014cd0..b9870aea 100644
--- a/packages/indiekit/lib/post-types.js
+++ b/packages/indiekit/lib/post-types.js
@@ -3,26 +3,23 @@ import _ from "lodash";
 /**
  * Get merged preset and custom post types
  * @param {object} publication - Publication configuration
- * @param {Array[object]} publication.postTypes - Publication post types
+ * @param {object} publication.postTypes - Publication post types
  * @param {object} [publication.preset] - Publication preset
  * @returns {object} Merged configuration
  */
 export const getPostTypes = ({ postTypes, preset }) => {
-  postTypes = _.keyBy(postTypes, "type");
-
   if (preset?.postTypes) {
-    const presetPostTypes = _.keyBy(preset.postTypes, "type");
-
-    postTypes = _.merge(postTypes, presetPostTypes);
+    postTypes = _.merge(postTypes, preset.postTypes);
   }
 
   // Add fallback values to post type if not provided
-  for (const key of Object.keys(postTypes)) {
-    const { fields, name, requiredFields, type } = postTypes[key];
-    postTypes[key].name = name || _.upperFirst(type);
-    postTypes[key].fields = fields || [];
-    postTypes[key].requiredFields = requiredFields || [];
+  for (const type of Object.keys(postTypes)) {
+    const { fields, name, requiredFields } = postTypes[type];
+    postTypes[type].type = type;
+    postTypes[type].name = name || _.upperFirst(type);
+    postTypes[type].fields = fields || [];
+    postTypes[type].requiredFields = requiredFields || [];
   }
 
-  return _.values(postTypes);
+  return postTypes;
 };
diff --git a/packages/indiekit/views/status.njk b/packages/indiekit/views/status.njk
index 290b043d..7a565c11 100644
--- a/packages/indiekit/views/status.njk
+++ b/packages/indiekit/views/status.njk
@@ -1,10 +1,10 @@
 {% extends "document.njk" %}
 
 {%- set postTypesHtml %}
-  <ul role="list">{% for config in publication.postTypes %}
+  <ul role="list">{% for type, config in publication.postTypes | dictsort %}
     <li>
-      {{ icon(config.type) or icon("note") }}{{ config.name }}
-      <small><code class="token attr-name">{{ config.type }}</code></small>
+      {{ icon(type) or icon("note") }}{{ config.name }}
+      <small><code class="token attr-name">{{ type }}</code></small>
     </li>
   {% endfor %}</ul>
 {% endset -%}
@@ -74,7 +74,7 @@
         value: {
           text: postTypesHtml | indent(4)
         }
-      } if publication.postTypes.length > 0, {
+      }, {
         key: {
           text: __("status.publication.preset")
         },
diff --git a/packages/preset-hugo/lib/post-types.js b/packages/preset-hugo/lib/post-types.js
index e6476fb1..873e5263 100644
--- a/packages/preset-hugo/lib/post-types.js
+++ b/packages/preset-hugo/lib/post-types.js
@@ -6,10 +6,7 @@ import plur from "plur";
  * @returns {object} Updated post type configuration
  */
 export const getPostTypes = (postTypes) => {
-  const types = [];
-
-  for (const postType of postTypes) {
-    const { type } = postType;
+  for (const type of Object.keys(postTypes)) {
     const section = plur(type);
 
     /**
@@ -17,8 +14,8 @@ export const getPostTypes = (postTypes) => {
      * @see {@link https://gohugo.io/content-management/organization/}
      * @see {@link https://gohugo.io/content-management/static-files/}
      */
-    types.push({
-      type,
+    postTypes[type] = {
+      ...postTypes[type],
       post: {
         path: `content/${section}/{slug}.md`,
         url: `${section}/{slug}`,
@@ -27,8 +24,8 @@ export const getPostTypes = (postTypes) => {
         path: `static/${section}/{filename}`,
         url: `${section}/{filename}`,
       },
-    });
+    };
   }
 
-  return types;
+  return postTypes;
 };
diff --git a/packages/preset-jekyll/lib/post-types.js b/packages/preset-jekyll/lib/post-types.js
index 5690ad4c..85e27357 100644
--- a/packages/preset-jekyll/lib/post-types.js
+++ b/packages/preset-jekyll/lib/post-types.js
@@ -6,10 +6,7 @@ import plur from "plur";
  * @returns {object} Updated post type configuration
  */
 export const getPostTypes = (postTypes) => {
-  const types = [];
-
-  for (const postType of postTypes) {
-    const { type } = postType;
+  for (const type of Object.keys(postTypes)) {
     const collection = plur(type);
 
     if (type === "article") {
@@ -17,8 +14,8 @@ export const getPostTypes = (postTypes) => {
        * Posts use `_posts` folder
        * @see {@link https://jekyllrb.com/docs/posts/}
        */
-      types.push({
-        type,
+      postTypes.article = {
+        ...postTypes.article,
         post: {
           path: "_posts/{yyyy}-{MM}-{dd}-{slug}.md",
           url: "{yyyy}/{MM}/{dd}/{slug}",
@@ -26,14 +23,14 @@ export const getPostTypes = (postTypes) => {
         media: {
           path: "media/{yyyy}/{MM}/{dd}/{filename}",
         },
-      });
+      };
     } else {
       /**
        * Other post types use collection folders
        * @see {@link https://jekyllrb.com/docs/collections/}
        */
-      types.push({
-        type,
+      postTypes[type] = {
+        ...postTypes[type],
         post: {
           path: `_${collection}/{yyyy}-{MM}-{dd}-{slug}.md`,
           url: `${collection}/{yyyy}/{MM}/{dd}/{slug}`,
@@ -41,9 +38,9 @@ export const getPostTypes = (postTypes) => {
         media: {
           path: `media/${collection}/{yyyy}/{MM}/{dd}/{filename}`,
         },
-      });
+      };
     }
   }
 
-  return types;
+  return postTypes;
 };


Test patch:
diff --git a/packages/endpoint-media/test/unit/media-data.js b/packages/endpoint-media/test/unit/media-data.js
index 4495e165..8bc66682 100644
--- a/packages/endpoint-media/test/unit/media-data.js
+++ b/packages/endpoint-media/test/unit/media-data.js
@@ -60,7 +60,7 @@ describe("endpoint-media/lib/media-data", async () => {
   });
 
   it("Throws error creating media data for non-configured media type", async () => {
-    publication.postTypes = [];
+    publication.postTypes = {};
 
     await assert.rejects(mediaData.create(application, publication, file), {
       message: "photo",
diff --git a/packages/endpoint-media/test/unit/utils.js b/packages/endpoint-media/test/unit/utils.js
index 4522e796..d5344c35 100644
--- a/packages/endpoint-media/test/unit/utils.js
+++ b/packages/endpoint-media/test/unit/utils.js
@@ -1,7 +1,6 @@
 import { strict as assert } from "node:assert";
 import { before, describe, it, mock } from "node:test";
-import { testConfig } from "@indiekit-test/config";
-import { getPostTypeConfig, renderPath } from "../../lib/utils.js";
+import { renderPath } from "../../lib/utils.js";
 
 describe("endpoint-media/lib/util", () => {
   before(() => {
@@ -9,13 +8,6 @@ describe("endpoint-media/lib/util", () => {
     mock.method(console, "warn", () => {});
   });
 
-  it("Get post type configuration for a given type", async () => {
-    const { publication } = await testConfig({ usePostTypes: true });
-    const result = getPostTypeConfig("note", publication.postTypes);
-
-    assert.equal(result.name, "Custom note post type");
-  });
-
   it("Renders path from URI template and properties", async () => {
     const template = "{yyyy}/{MM}/{uuid}/{slug}";
     const properties = {
diff --git a/packages/endpoint-micropub/test/unit/post-data.js b/packages/endpoint-micropub/test/unit/post-data.js
index db60b8c3..f26657ef 100644
--- a/packages/endpoint-micropub/test/unit/post-data.js
+++ b/packages/endpoint-micropub/test/unit/post-data.js
@@ -58,7 +58,7 @@ describe("endpoint-micropub/lib/post-data", async () => {
   });
 
   it("Throws error creating post data for non-configured post type", async () => {
-    publication.postTypes = [];
+    publication.postTypes = {};
 
     await assert.rejects(
       postData.create(application, publication, properties),
diff --git a/packages/endpoint-micropub/test/unit/post-type-discovery.js b/packages/endpoint-micropub/test/unit/post-type-discovery.js
index 876cb820..5e21e208 100644
--- a/packages/endpoint-micropub/test/unit/post-type-discovery.js
+++ b/packages/endpoint-micropub/test/unit/post-type-discovery.js
@@ -3,10 +3,10 @@ import { describe, it } from "node:test";
 import { getPostType } from "../../lib/post-type-discovery.js";
 
 describe("endpoint-media/lib/post-type-discovery", () => {
-  const postTypes = [
-    { type: "audio", discovery: "audio" },
-    { type: "bookmark", discovery: "bookmark-of" },
-  ];
+  const postTypes = {
+    audio: { discovery: "audio" },
+    bookmark: { discovery: "bookmark-of" },
+  };
 
   it("Discovers note post type", () => {
     const result = getPostType(postTypes, {
diff --git a/packages/endpoint-micropub/test/unit/utils.js b/packages/endpoint-micropub/test/unit/utils.js
index ca3d6e36..362ba997 100644
--- a/packages/endpoint-micropub/test/unit/utils.js
+++ b/packages/endpoint-micropub/test/unit/utils.js
@@ -1,10 +1,8 @@
 import { strict as assert } from "node:assert";
 import { before, describe, it, mock } from "node:test";
-import { testConfig } from "@indiekit-test/config";
 import {
   decodeQueryParameter,
   excerptString,
-  getPostTypeConfig,
   getPostTemplateProperties,
   relativeMediaPath,
   renderPath,
@@ -41,13 +39,6 @@ describe("endpoint-media/lib/utils", () => {
     });
   });
 
-  it("Gets post type configuration for a given type", async () => {
-    const { publication } = await testConfig({ usePostTypes: true });
-    const result = getPostTypeConfig("note", publication.postTypes);
-
-    assert.equal(result.name, "Custom note post type");
-  });
-
   it("Renders relative path if at publication URL", () => {
     const result = relativeMediaPath("http://foo.bar/media/", "http://foo.bar");
 
diff --git a/packages/endpoint-posts/test/unit/utils.js b/packages/endpoint-posts/test/unit/utils.js
index ff01bb8d..4566682d 100644
--- a/packages/endpoint-posts/test/unit/utils.js
+++ b/packages/endpoint-posts/test/unit/utils.js
@@ -7,18 +7,16 @@ import {
   getLocationProperty,
   getPostName,
   getPostStatusBadges,
-  getPostTypeConfig,
   getPostUrl,
   getSyndicateToItems,
 } from "../../lib/utils.js";
 
 const publication = {
-  postTypes: [
-    {
-      type: "article",
+  postTypes: {
+    article: {
       name: "Journal entry",
     },
-  ],
+  },
   syndicationTargets: [
     {
       info: {
@@ -217,14 +215,6 @@ describe("endpoint-posts/lib/utils", () => {
     ]);
   });
 
-  it("Gets post type name (or an empty string)", () => {
-    assert.deepEqual(getPostTypeConfig(publication, "article"), {
-      type: "article",
-      name: "Journal entry",
-    });
-    assert.equal(getPostTypeConfig(publication, ""), "");
-  });
-
   it("Gets post URL", () => {
     assert.equal(
       getPostUrl("aHR0cHM6Ly93ZWJzaXRlLmV4YW1wbGUvZm9vYmFy"),
diff --git a/packages/indiekit/test/unit/post-types.js b/packages/indiekit/test/unit/post-types.js
index 25137a91..af567f33 100644
--- a/packages/indiekit/test/unit/post-types.js
+++ b/packages/indiekit/test/unit/post-types.js
@@ -12,7 +12,7 @@ describe("indiekit/lib/post-types", () => {
     });
     const indiekit = await Indiekit.initialize({ config });
     const { publication } = await indiekit.bootstrap();
-    const result = getPostTypes(publication)[0];
+    const { article: result } = getPostTypes(publication);
 
     assert.equal(result.name, "Article");
     assert.equal(result.type, "article");
@@ -33,9 +33,9 @@ describe("indiekit/lib/post-types", () => {
     const config = await testConfig({ usePostTypes: true });
     const indiekit = await Indiekit.initialize({ config });
     const { publication } = await indiekit.bootstrap();
-    const result = getPostTypes(publication);
+    const { note: result } = getPostTypes(publication);
 
-    assert.equal(result[1].name, "Custom note post type");
+    assert.equal(result.name, "Custom note post type");
   });
 
   it("Returns default if no preset or custom post types", async () => {
@@ -47,7 +47,7 @@ describe("indiekit/lib/post-types", () => {
     const { publication } = await indiekit.bootstrap();
     const result = getPostTypes(publication);
 
-    assert.equal(result[0].name, "Article");
-    assert.equal(result[10].name, "Like");
+    assert.equal(result.article.name, "Article");
+    assert.equal(result.like.name, "Like");
   });
 });
diff --git a/packages/preset-hugo/test/index.js b/packages/preset-hugo/test/index.js
index b4f66a4b..005a5aad 100644
--- a/packages/preset-hugo/test/index.js
+++ b/packages/preset-hugo/test/index.js
@@ -8,12 +8,11 @@ describe("preset-hugo", async () => {
   const indiekit = await Indiekit.initialize({
     config: {
       publication: {
-        postTypes: [
-          {
-            type: "puppy",
+        postTypes: {
+          puppy: {
             name: "Puppy posts",
           },
-        ],
+        },
       },
     },
   });
@@ -37,7 +36,7 @@ describe("preset-hugo", async () => {
   });
 
   it("Gets publication post types", () => {
-    assert.deepEqual(hugo.postTypes[0].post, {
+    assert.deepEqual(hugo.postTypes.article.post, {
       path: "content/articles/{slug}.md",
       url: "articles/{slug}",
     });
diff --git a/packages/preset-hugo/test/unit/post-types.js b/packages/preset-hugo/test/unit/post-types.js
index dd4b4872..79dd5712 100644
--- a/packages/preset-hugo/test/unit/post-types.js
+++ b/packages/preset-hugo/test/unit/post-types.js
@@ -2,26 +2,23 @@ import { strict as assert } from "node:assert";
 import { describe, it } from "node:test";
 import { getPostTypes } from "../../lib/post-types.js";
 
-const postTypes = [
-  {
-    type: "article",
+const postTypes = {
+  article: {
     name: "Journal post",
   },
-  {
-    type: "note",
+  note: {
     name: "Micro post",
   },
-  {
-    type: "puppy",
+  puppy: {
     name: "Puppy post",
   },
-];
+};
 
 describe("preset-hugo/lib/post-types", () => {
   it("Gets paths and URLs for configured post types", () => {
-    assert.deepEqual(getPostTypes(postTypes), [
-      {
-        type: "article",
+    assert.deepEqual(getPostTypes(postTypes), {
+      article: {
+        name: "Journal post",
         post: {
           path: "content/articles/{slug}.md",
           url: "articles/{slug}",
@@ -31,8 +28,8 @@ describe("preset-hugo/lib/post-types", () => {
           url: "articles/{filename}",
         },
       },
-      {
-        type: "note",
+      note: {
+        name: "Micro post",
         post: {
           path: "content/notes/{slug}.md",
           url: "notes/{slug}",
@@ -42,8 +39,8 @@ describe("preset-hugo/lib/post-types", () => {
           url: "notes/{filename}",
         },
       },
-      {
-        type: "puppy",
+      puppy: {
+        name: "Puppy post",
         post: {
           path: "content/puppies/{slug}.md",
           url: "puppies/{slug}",
@@ -53,6 +50,6 @@ describe("preset-hugo/lib/post-types", () => {
           url: "puppies/{filename}",
         },
       },
-    ]);
+    });
   });
 });
diff --git a/packages/preset-jekyll/test/index.js b/packages/preset-jekyll/test/index.js
index a6768021..e0fc661a 100644
--- a/packages/preset-jekyll/test/index.js
+++ b/packages/preset-jekyll/test/index.js
@@ -8,12 +8,11 @@ describe("preset-jekyll", async () => {
   const indiekit = await Indiekit.initialize({
     config: {
       publication: {
-        postTypes: [
-          {
-            type: "puppy",
+        postTypes: {
+          puppy: {
             name: "Puppy posts",
           },
-        ],
+        },
       },
     },
   });
@@ -30,7 +29,7 @@ describe("preset-jekyll", async () => {
   });
 
   it("Gets publication post types", () => {
-    assert.deepEqual(jekyll.postTypes[0].post, {
+    assert.deepEqual(jekyll.postTypes.article.post, {
       path: "_posts/{yyyy}-{MM}-{dd}-{slug}.md",
       url: "{yyyy}/{MM}/{dd}/{slug}",
     });
diff --git a/packages/preset-jekyll/test/unit/post-types.js b/packages/preset-jekyll/test/unit/post-types.js
index 0ec8634a..cc1cb0c5 100644
--- a/packages/preset-jekyll/test/unit/post-types.js
+++ b/packages/preset-jekyll/test/unit/post-types.js
@@ -2,26 +2,23 @@ import { strict as assert } from "node:assert";
 import { describe, it } from "node:test";
 import { getPostTypes } from "../../lib/post-types.js";
 
-const postTypes = [
-  {
-    type: "article",
+const postTypes = {
+  article: {
     name: "Journal post",
   },
-  {
-    type: "note",
+  note: {
     name: "Micro post",
   },
-  {
-    type: "puppy",
+  puppy: {
     name: "Puppy post",
   },
-];
+};
 
 describe("preset-jekyll/lib/post-types", () => {
   it("Gets paths and URLs for configured post types", () => {
-    assert.deepEqual(getPostTypes(postTypes), [
-      {
-        type: "article",
+    assert.deepEqual(getPostTypes(postTypes), {
+      article: {
+        name: "Journal post",
         post: {
           path: "_posts/{yyyy}-{MM}-{dd}-{slug}.md",
           url: "{yyyy}/{MM}/{dd}/{slug}",
@@ -30,8 +27,8 @@ describe("preset-jekyll/lib/post-types", () => {
           path: "media/{yyyy}/{MM}/{dd}/{filename}",
         },
       },
-      {
-        type: "note",
+      note: {
+        name: "Micro post",
         post: {
           path: "_notes/{yyyy}-{MM}-{dd}-{slug}.md",
           url: "notes/{yyyy}/{MM}/{dd}/{slug}",
@@ -40,8 +37,8 @@ describe("preset-jekyll/lib/post-types", () => {
           path: "media/notes/{yyyy}/{MM}/{dd}/{filename}",
         },
       },
-      {
-        type: "puppy",
+      puppy: {
+        name: "Puppy post",
         post: {
           path: "_puppies/{yyyy}-{MM}-{dd}-{slug}.md",
           url: "puppies/{yyyy}/{MM}/{dd}/{slug}",
@@ -50,6 +47,6 @@ describe("preset-jekyll/lib/post-types", () => {
           path: "media/puppies/{yyyy}/{MM}/{dd}/{filename}",
         },
       },
-    ]);
+    });
   });
 });