From e5a05c6e6a8dc118948a3a9411d58cedfacdc799 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Sun, 6 Sep 2026 15:44:23 +0200 Subject: [PATCH] feat: add page post type A page is an undated post at the root of a website, such as /about or /now. The package follows the other post-type packages; the presets give a page an undated root path where each generator serves it. Fixes #954 --- docs/.vitepress/config.js | 4 ++ docs/plugins/post-types/index.md | 1 + docs/plugins/post-types/page.md | 1 + .../frontend/views/frontend-components.njk | 1 + indiekit.config.js | 1 + package-lock.json | 12 ++++++ .../integration/202-action-create-page.js | 31 +++++++++++++++ packages/frontend/lib/globals/icon.js | 1 + packages/post-type-page/README.md | 29 ++++++++++++++ packages/post-type-page/assets/icon.svg | 4 ++ packages/post-type-page/index.js | 31 +++++++++++++++ packages/post-type-page/package.json | 38 +++++++++++++++++++ packages/post-type-photo/package.json | 6 +++ packages/preset-eleventy/lib/post-types.js | 15 ++++++++ .../preset-eleventy/test/unit/post-types.js | 11 ++++++ packages/preset-hugo/lib/post-types.js | 16 ++++++++ packages/preset-hugo/test/unit/post-types.js | 12 ++++++ packages/preset-jekyll/lib/post-types.js | 15 +++++++- .../preset-jekyll/test/unit/post-types.js | 11 ++++++ 19 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 docs/plugins/post-types/page.md create mode 100644 packages/endpoint-micropub/test/integration/202-action-create-page.js create mode 100644 packages/post-type-page/README.md create mode 100644 packages/post-type-page/assets/icon.svg create mode 100644 packages/post-type-page/index.js create mode 100644 packages/post-type-page/package.json diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 3d851e7ff..2b8b1efea 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -185,6 +185,10 @@ const sidebarPlugins = [ text: "Note", link: "/plugins/post-types/note", }, + { + text: "Page", + link: "/plugins/post-types/page", + }, { text: "Photo", link: "/plugins/post-types/photo", diff --git a/docs/plugins/post-types/index.md b/docs/plugins/post-types/index.md index 039115cd0..02db072da 100644 --- a/docs/plugins/post-types/index.md +++ b/docs/plugins/post-types/index.md @@ -16,6 +16,7 @@ A [post type](../../concepts#post-type) provides an interface for creating and e - [Audio](audio.md) `@indiekit/post-type-audio` - [Event](event.md) `@indiekit/post-type-event` - [Jam](jam.md) `@indiekit/post-type-jam` +- [Page](page.md) `@indiekit/post-type-page` - [Repost](repost.md) `@indiekit/post-type-repost` - [RSVP](rsvp.md) `@indiekit/post-type-rsvp` - [Video](video.md) `@indiekit/post-type-video` diff --git a/docs/plugins/post-types/page.md b/docs/plugins/post-types/page.md new file mode 100644 index 000000000..d47144312 --- /dev/null +++ b/docs/plugins/post-types/page.md @@ -0,0 +1 @@ + diff --git a/helpers/frontend/views/frontend-components.njk b/helpers/frontend/views/frontend-components.njk index fa222b4f2..eac513ab4 100644 --- a/helpers/frontend/views/frontend-components.njk +++ b/helpers/frontend/views/frontend-components.njk @@ -311,6 +311,7 @@
  • {{- icon("next") -}} next
  • {{- icon("note") -}} note
  • {{- icon("offline") -}} offline
  • +
  • {{- icon("page") -}} page
  • {{- icon("photo") -}} photo
  • {{- icon("previous") -}} previous
  • {{- icon("public") -}} public
  • diff --git a/indiekit.config.js b/indiekit.config.js index 70eadc6bd..9d2a0c6bd 100644 --- a/indiekit.config.js +++ b/indiekit.config.js @@ -22,6 +22,7 @@ const config = { "@indiekit/post-type-audio", "@indiekit/post-type-event", "@indiekit/post-type-jam", + "@indiekit/post-type-page", "@indiekit/post-type-repost", "@indiekit/post-type-rsvp", "@indiekit/post-type-video", diff --git a/package-lock.json b/package-lock.json index f1180459f..79ee4c4ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5104,6 +5104,10 @@ "resolved": "packages/post-type-note", "link": true }, + "node_modules/@indiekit/post-type-page": { + "resolved": "packages/post-type-page", + "link": true + }, "node_modules/@indiekit/post-type-photo": { "resolved": "packages/post-type-photo", "link": true @@ -29555,6 +29559,14 @@ "node": ">=24.17" } }, + "packages/post-type-page": { + "name": "@indiekit/post-type-page", + "version": "1.0.0-beta.28", + "license": "MIT", + "engines": { + "node": ">=24.17" + } + }, "packages/post-type-photo": { "name": "@indiekit/post-type-photo", "version": "1.0.0-beta.28", diff --git a/packages/endpoint-micropub/test/integration/202-action-create-page.js b/packages/endpoint-micropub/test/integration/202-action-create-page.js new file mode 100644 index 000000000..6c63024e9 --- /dev/null +++ b/packages/endpoint-micropub/test/integration/202-action-create-page.js @@ -0,0 +1,31 @@ +import { strict as assert } from "node:assert"; +import { after, describe, it } from "node:test"; + +import { mockAgent } from "@indiekit-test/mock-agent"; +import { testServer } from "@indiekit-test/server"; +import { testToken } from "@indiekit-test/token"; +import supertest from "supertest"; + +await mockAgent("endpoint-micropub"); +const server = await testServer({ + plugins: ["@indiekit/post-type-page", "@indiekit/preset-eleventy"], + usePostTypes: false, +}); +const request = supertest.agent(server); + +describe("endpoint-micropub POST /micropub", () => { + it("Creates page at root URL", async () => { + const result = await request + .post("/micropub") + .auth(testToken(), { type: "bearer" }) + .set("accept", "application/json") + .send("h=page") + .send("name=About") + .send("content=All+about+me."); + + assert.equal(result.status, 202); + assert.equal(result.headers.location, "https://website.example/about"); + }); + + after(() => server.close()); +}); diff --git a/packages/frontend/lib/globals/icon.js b/packages/frontend/lib/globals/icon.js index ab7ca2d24..df3b09580 100644 --- a/packages/frontend/lib/globals/icon.js +++ b/packages/frontend/lib/globals/icon.js @@ -30,6 +30,7 @@ export const icon = (name, title) => { note: "M12 8h24c2 0 4 2 4 4v24c0 2-2 4-4 4H12c-2 0-4-2-4-4V12c0-2 2-4 4-4zm24-4H12c-4 0-8 4-8 8v24c0 5 4 8 8 8h24c5 0 8-3 8-8V12c0-4-3-8-8-8zM12 14h24v4H12v-4zm0 8h24v4H12v-4zm0 8h18v4H12v-4z", offline: "M24 7a32 32 0 1 1-15.8 4.2l3.7 3.7a24.7 24.7 0 0 0-8.4 6.6L0 18v8.6a27 27 0 1 0 48 0V18l-3.5 3.5a27 27 0 0 0-25.1-9.1l-4.2-4.2A32 32 0 0 1 24 7Zm0 51a17 17 0 0 0 15.5-10h5.4A22 22 0 0 1 3 48h5.4A17 17 0 0 0 24 58Zm0-26a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm16-6-3.5 3.5-1.2-1.2-9.2-9.2a22 22 0 0 1 14 6.9Zm-22.9-6 4.1 4.1a17 17 0 0 0-9.7 5.3L8 26a22 22 0 0 1 9-6ZM2 5l3-3 41 41-3 3L2 5Z", + page: "M12 4h18l3 1 10 10 1 3v18q0 8-8 8H12q-7 0-8-8V12q1-7 8-8m18 4H12q-3 1-4 4v24q1 4 4 4h24q4 0 4-4V18H30zm-18 6h14v4H12zm0 8h24v4H12Zm0 8h24v4H12Z", photo: "M36 4H12c-4 0-8 4-8 8v24c0 5 4 8 8 8h24c5 0 8-3 8-8V12c0-4-3-8-8-8zM12 8h24c2 0 4 2 4 4v24c0 2-2 4-4 4H12c-2 0-4-2-4-4V12c0-2 2-4 4-4zm24 24l-9-14-7 11-3-5-5 8h24z", previous: diff --git a/packages/post-type-page/README.md b/packages/post-type-page/README.md new file mode 100644 index 000000000..12c1d3947 --- /dev/null +++ b/packages/post-type-page/README.md @@ -0,0 +1,29 @@ +# @indiekit/post-type-page + +Page post type for Indiekit. A page is an undated post at the root of your website, such as `/about`, `/now` or `/uses`. + +## Installation + +`npm install @indiekit/post-type-page` + +## Usage + +Add `@indiekit/post-type-page` to your list of plug-ins, specifying options as required: + +```json +{ + "plugins": ["@indiekit/post-type-page"], + "@indiekit/post-type-page": { + "name": "Page" + } +} +``` + +Micropub clients create a page by sending `h=page`. Presets write a page to the root of your content, where your static site generator serves it at `/{slug}`. + +## Options + +| Option | Type | Description | +| :------- | :------- | :-------------------------------- | +| `name` | `string` | Post type name. | +| `fields` | `Array` | Fields to show in post interface. | diff --git a/packages/post-type-page/assets/icon.svg b/packages/post-type-page/assets/icon.svg new file mode 100644 index 000000000..fa763105f --- /dev/null +++ b/packages/post-type-page/assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/post-type-page/index.js b/packages/post-type-page/index.js new file mode 100644 index 000000000..ea8af5cdd --- /dev/null +++ b/packages/post-type-page/index.js @@ -0,0 +1,31 @@ +const defaults = { + name: "Page", + fields: { + name: { required: true }, + content: { required: true }, + summary: {}, + category: {}, + "post-status": {}, + visibility: {}, + }, +}; + +export default class PagePostType { + name = "Page post type"; + + constructor(options = {}) { + this.options = { ...defaults, ...options }; + } + + get config() { + return { + name: this.options.name, + h: "page", + fields: this.options.fields, + }; + } + + init(Indiekit) { + Indiekit.addPostType("page", this); + } +} diff --git a/packages/post-type-page/package.json b/packages/post-type-page/package.json new file mode 100644 index 000000000..4c1968c7f --- /dev/null +++ b/packages/post-type-page/package.json @@ -0,0 +1,38 @@ +{ + "name": "@indiekit/post-type-page", + "version": "1.0.0-beta.28", + "description": "Page post type for Indiekit", + "keywords": [ + "indiekit", + "indiekit-plugin", + "indieweb", + "post-type", + "page" + ], + "homepage": "https://getindiekit.com", + "author": { + "name": "Paul Robert Lloyd", + "url": "https://paulrobertlloyd.com" + }, + "license": "MIT", + "engines": { + "node": ">=24.17" + }, + "type": "module", + "main": "index.js", + "files": [ + "assets", + "index.js" + ], + "bugs": { + "url": "https://github.com/getindiekit/indiekit/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/getindiekit/indiekit.git", + "directory": "packages/post-type-page" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/post-type-photo/package.json b/packages/post-type-photo/package.json index 99827e207..d01d3af84 100644 --- a/packages/post-type-photo/package.json +++ b/packages/post-type-photo/package.json @@ -14,6 +14,12 @@ "name": "Paul Robert Lloyd", "url": "https://paulrobertlloyd.com" }, + "contributors": [ + { + "name": "Ricardo Mendes", + "url": "https://rmendes.net/" + } + ], "license": "MIT", "engines": { "node": ">=24.17" diff --git a/packages/preset-eleventy/lib/post-types.js b/packages/preset-eleventy/lib/post-types.js index 47948f31d..5623a1757 100644 --- a/packages/preset-eleventy/lib/post-types.js +++ b/packages/preset-eleventy/lib/post-types.js @@ -7,6 +7,21 @@ import plur from "plur"; */ export const getPostTypes = (postTypes) => { for (const type of postTypes.keys()) { + // A page has no date and lives at the root, where Eleventy serves it + if (type === "page") { + postTypes.set(type, { + ...postTypes.get(type), + post: { + path: "{slug}.md", + url: "{slug}", + }, + media: { + path: "media/pages/{filename}", + }, + }); + continue; + } + const collection = plur(type); postTypes.set(type, { diff --git a/packages/preset-eleventy/test/unit/post-types.js b/packages/preset-eleventy/test/unit/post-types.js index 66b2b99fa..203ad19bd 100644 --- a/packages/preset-eleventy/test/unit/post-types.js +++ b/packages/preset-eleventy/test/unit/post-types.js @@ -7,6 +7,7 @@ const postTypes = new Map([ ["article", { name: "Journal post" }], ["note", { name: "Micro post" }], ["puppy", { name: "Puppy post" }], + ["page", { name: "Page" }], ]); describe("preset-eleventy/lib/post-types", () => { @@ -43,5 +44,15 @@ describe("preset-eleventy/lib/post-types", () => { path: "media/puppies/{yyyy}/{MM}/{dd}/{filename}", }, }); + assert.deepEqual(result.get("page"), { + name: "Page", + post: { + path: "{slug}.md", + url: "{slug}", + }, + media: { + path: "media/pages/{filename}", + }, + }); }); }); diff --git a/packages/preset-hugo/lib/post-types.js b/packages/preset-hugo/lib/post-types.js index fc7bd695b..18311e36f 100644 --- a/packages/preset-hugo/lib/post-types.js +++ b/packages/preset-hugo/lib/post-types.js @@ -7,6 +7,22 @@ import plur from "plur"; */ export const getPostTypes = (postTypes) => { for (const type of postTypes.keys()) { + // A page has no date and lives at the root of the content directory + if (type === "page") { + postTypes.set(type, { + ...postTypes.get(type), + post: { + path: "content/{slug}.md", + url: "{slug}", + }, + media: { + path: "static/pages/{filename}", + url: "pages/{filename}", + }, + }); + continue; + } + const section = plur(type); /** diff --git a/packages/preset-hugo/test/unit/post-types.js b/packages/preset-hugo/test/unit/post-types.js index 0764ecafa..c18b83dc7 100644 --- a/packages/preset-hugo/test/unit/post-types.js +++ b/packages/preset-hugo/test/unit/post-types.js @@ -7,6 +7,7 @@ const postTypes = new Map([ ["article", { name: "Journal post" }], ["note", { name: "Micro post" }], ["puppy", { name: "Puppy post" }], + ["page", { name: "Page" }], ]); describe("preset-hugo/lib/post-types", () => { @@ -46,5 +47,16 @@ describe("preset-hugo/lib/post-types", () => { url: "puppies/{filename}", }, }); + assert.deepEqual(result.get("page"), { + name: "Page", + post: { + path: "content/{slug}.md", + url: "{slug}", + }, + media: { + path: "static/pages/{filename}", + url: "pages/{filename}", + }, + }); }); }); diff --git a/packages/preset-jekyll/lib/post-types.js b/packages/preset-jekyll/lib/post-types.js index e9c9f62b2..43d2d0dbe 100644 --- a/packages/preset-jekyll/lib/post-types.js +++ b/packages/preset-jekyll/lib/post-types.js @@ -16,7 +16,20 @@ export const getPostTypes = (postTypes) => { for (const type of postTypes.keys()) { const collection = plur(type); - if (type === "article") { + if (type === "page") { + // A page has no date; an index file in its own directory is served at + // its URL without a permalink setting + updates.set("page", { + ...postTypes.get("page"), + post: { + path: "{slug}/index.md", + url: "{slug}", + }, + media: { + path: "media/pages/{filename}", + }, + }); + } else if (type === "article") { updates.set("article", { ...postTypes.get("article"), post: { diff --git a/packages/preset-jekyll/test/unit/post-types.js b/packages/preset-jekyll/test/unit/post-types.js index 207a2f618..07f76d882 100644 --- a/packages/preset-jekyll/test/unit/post-types.js +++ b/packages/preset-jekyll/test/unit/post-types.js @@ -7,6 +7,7 @@ const postTypes = new Map([ ["article", { name: "Journal post" }], ["note", { name: "Micro post" }], ["puppy", { name: "Puppy post" }], + ["page", { name: "Page" }], ]); describe("preset-jekyll/lib/post-types", () => { @@ -43,5 +44,15 @@ describe("preset-jekyll/lib/post-types", () => { path: "media/puppies/{yyyy}/{MM}/{dd}/{filename}", }, }); + assert.deepEqual(result.get("page"), { + name: "Page", + post: { + path: "{slug}/index.md", + url: "{slug}", + }, + media: { + path: "media/pages/{filename}", + }, + }); }); });