Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Micropub channels #774

Merged
merged 5 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/configuration/publication.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,44 @@ export default {

:::

## `channels`

A keyed collection of configuration properties for different channels, which can be used to organise posts within your publication. For example:

::: code-group

```json [JSON]
{
"publication": {
"channels": {
"posts": {
"name": "Posts"
},
"pages": {
"name": "Pages"
}
}
}
}
```

```js [JavaScript]
export default {
publication: {
channels: {
posts: {
name: "Posts"
},
pages: {
name: "Pages"
}
}
}
}
```

:::

## `enrichPostData`

A boolean to determine if data about URLs referenced in new posts is fetched and appended to a post’s properties.
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ The following tokens are only available for post files:

| Token | Description |
| :---- | :---------- |
| `channel` | Channel provided in `mp-channel` property, else default channel UID. Token only available if `publication.channels` is configured. |
| `slug` | Slug provided in `mp-slug` property, else slugified `name` property, else a 5 character string, for example `ycf9o` |

### Media file tokens
Expand Down
2 changes: 2 additions & 0 deletions docs/specifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ The following [scopes](https://indieweb.org/scope) are supported:
### Extensions

* [x] [Category](https://github.com/indieweb/micropub-extensions/issues/5) query
* [x] [Channel](https://github.com/indieweb/micropub-extensions/issues/40) query
* [x] [Post list](https://github.com/indieweb/micropub-extensions/issues/4) query
* [x] [Supported properties](https://github.com/indieweb/micropub-extensions/issues/33) query
* [x] [Supported queries](https://github.com/indieweb/micropub-extensions/issues/7) query
Expand All @@ -71,6 +72,7 @@ The following [scopes](https://indieweb.org/scope) are supported:

### Server commands

* [x] `mp-channel`: Channel(s) to use for a published post
* [x] `mp-photo-alt`: Alternative text to use for a published photo
* [x] `mp-slug`: URL slug to use in published post
* [x] `mp-syndicate-to`: Which syndication targets to syndicate post to
Expand Down
8 changes: 8 additions & 0 deletions indiekit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ const config = {
publication: {
me: process.env.PUBLICATION_URL,
categories: ["internet", "indieweb", "indiekit", "test", "testing"],
channels: {
posts: {
name: "Posts",
},
pages: {
name: "Pages",
},
},
enrichPostData: true,
postTypes: {
like: {
Expand Down
7 changes: 6 additions & 1 deletion packages/endpoint-micropub/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
*/
export const getConfig = (application, publication) => {
const { mediaEndpoint, url } = application;
const { categories, postTypes, syndicationTargets } = publication;
const { categories, channels, postTypes, syndicationTargets } = publication;

// Supported queries
const q = [
"category",
"channel",
"config",
"media-endpoint",
"post-types",
Expand All @@ -28,6 +29,10 @@ export const getConfig = (application, publication) => {

return {
categories,
channels: Object.entries(channels).map(([uid, channel]) => ({
uid,
name: channel.name,
})),
"media-endpoint": mediaEndpoint,
"post-types": Object.values(postTypes).map((postType) => ({
type: postType.type,
Expand Down
3 changes: 3 additions & 0 deletions packages/endpoint-micropub/lib/controllers/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export const queryController = async (request, response, next) => {
// `category` param is used to query `categories` configuration property
q = q === "category" ? "categories" : String(q);

// `channel` param is used to query `channels` configuration property
q = q === "channel" ? "channels" : String(q);

switch (q) {
case "config": {
response.json(config);
Expand Down
45 changes: 44 additions & 1 deletion packages/endpoint-micropub/lib/jf2.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const mf2ToJf2 = async (body, requestReferences) => {
* @returns {object} Normalised JF2 properties
*/
export const normaliseProperties = (publication, properties, timeZone) => {
const { me, slugSeparator } = publication;
const { channels, me, slugSeparator } = publication;

properties.published = getDate(timeZone, properties.published);

Expand Down Expand Up @@ -101,6 +101,11 @@ export const normaliseProperties = (publication, properties, timeZone) => {

properties.slug = getSlugProperty(properties, slugSeparator);

const publicationHasChannels = channels && Object.keys(channels).length > 0;
if (publicationHasChannels) {
properties["mp-channel"] = getChannelProperty(properties, channels);
}

if (properties["mp-syndicate-to"]) {
properties["mp-syndicate-to"] = toArray(properties["mp-syndicate-to"]);
}
Expand All @@ -127,6 +132,44 @@ export const getAudioProperty = (properties, me) => {
}));
};

/**
* Get channel property.
*
* If a publication has configured channels, but no channel has been selected,
* the default channel is used.
*
* If `mp-channel` provides a UID that does not appear in the publication’s
* channels, the default channel is used.
*
* The first item in a publication’s configured channels is considered the
* default channel.
* @param {object} properties - JF2 properties
* @param {object} channels - Publication channels
* @returns {Array} `mp-channel` property
* @see {@link https://github.com/indieweb/micropub-extensions/issues/40}
*/
export const getChannelProperty = (properties, channels) => {
channels = Object.keys(channels);
const mpChannel = properties["mp-channel"];
const providedChannels = Array.isArray(mpChannel) ? mpChannel : [mpChannel];
const selectedChannels = new Set();

// Only select channels that have been configured
for (const uid of providedChannels) {
if (channels.includes(uid)) {
selectedChannels.add(uid);
}
}

// If no channels provided, use default channel UID
if (selectedChannels.size === 0) {
const defaultChannel = channels[0];
selectedChannels.add(defaultChannel);
}

return toArray([...selectedChannels]);
};

/**
* Get content property.
*
Expand Down
12 changes: 11 additions & 1 deletion packages/endpoint-micropub/lib/post-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@ export const postData = {
typeConfig.post.path,
properties,
application,
publication,
);
const url = await renderPath(
typeConfig.post.url,
properties,
application,
publication,
);
const url = await renderPath(typeConfig.post.url, properties, application);
properties.url = getCanonicalUrl(url, me);

// Post status
Expand Down Expand Up @@ -144,11 +150,13 @@ export const postData = {
typeConfig.post.path,
properties,
application,
publication,
);
const updatedUrl = await renderPath(
typeConfig.post.url,
properties,
application,
publication,
);
properties.url = getCanonicalUrl(updatedUrl, me);

Expand Down Expand Up @@ -208,6 +216,7 @@ export const postData = {
typeConfig.post.path,
properties,
application,
publication,
);

// Update data in posts collection
Expand Down Expand Up @@ -249,6 +258,7 @@ export const postData = {
typeConfig.post.path,
properties,
application,
publication,
);

// Post status
Expand Down
21 changes: 20 additions & 1 deletion packages/endpoint-micropub/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export const getPostTemplateProperties = (properties) => {
const templateProperties = structuredClone(properties);

for (let key in templateProperties) {
// Pass mp-channel to template as channel
if (key === "mp-channel") {
templateProperties.channel = templateProperties["mp-channel"];
}

// Remove server commands from post template properties
if (key.startsWith("mp-")) {
delete templateProperties[key];
Expand Down Expand Up @@ -78,12 +83,19 @@ export const relativeMediaPath = (url, me) =>
* @param {string} path - URI template path
* @param {object} properties - JF2 properties
* @param {object} application - Application configuration
* @param {object} publication - Publication configuration
* @returns {Promise<string>} Path
*/
export const renderPath = async (path, properties, application) => {
export const renderPath = async (
path,
properties,
application,
publication,
) => {
const dateObject = new Date(properties.published);
const serverTimeZone = getTimeZoneDesignator();
const { locale, timeZone } = application;
const { slugSeparator } = publication;
let tokens = {};

// Add date tokens
Expand All @@ -105,6 +117,13 @@ export const renderPath = async (path, properties, application) => {
// Add slug token
tokens.slug = properties.slug;

// Add channel token
if (properties.channel) {
tokens.channel = Array.isArray(properties.channel)
? properties.channel.join(slugSeparator)
: properties.channel;
}

// Populate URI template path with properties
path = supplant(path, tokens);

Expand Down
32 changes: 32 additions & 0 deletions packages/endpoint-micropub/test/unit/jf2.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
formEncodedToJf2,
mf2ToJf2,
getAudioProperty,
getChannelProperty,
getContentProperty,
getLocationProperty,
getPhotoProperty,
Expand All @@ -17,6 +18,14 @@ import {

await mockAgent("endpoint-micropub");
const publication = {
channels: {
posts: {
name: "Posts",
},
pages: {
name: "Pages",
},
},
slugSeparator: "-",
syndicationTargets: [
{
Expand Down Expand Up @@ -132,6 +141,28 @@ describe("endpoint-micropub/lib/jf2", () => {
]);
});

it("Gets normalised channel property", () => {
const one = { "mp-channel": "posts" };
const none = { "mp-channel": "" };
const many = { "mp-channel": ["posts", "pages"] };
const { channels } = publication;

assert.deepEqual(getChannelProperty(one, channels), ["posts"]);
assert.deepEqual(getChannelProperty(none, channels), ["posts"]);
assert.deepEqual(getChannelProperty(many, channels), ["posts", "pages"]);
});

it("Gets normalised channel property, returning default channel", () => {
const oneMissing = { "mp-channel": "foo" };
const manyMissing = { "mp-channel": ["foo", "bar"] };
const someMissing = { "mp-channel": ["foo", "bar", "pages"] };
const { channels } = publication;

assert.deepEqual(getChannelProperty(oneMissing, channels), ["posts"]);
assert.deepEqual(getChannelProperty(manyMissing, channels), ["posts"]);
assert.deepEqual(getChannelProperty(someMissing, channels), ["pages"]);
});

it("Gets text and HTML values from `content` property", () => {
const properties = JSON.parse(
getFixture("jf2/article-content-provided-html-text.jf2"),
Expand Down Expand Up @@ -400,6 +431,7 @@ describe("endpoint-micropub/lib/jf2", () => {
);
const result = normaliseProperties(publication, properties, "UTC");

assert.equal(result.channels, undefined);
assert.equal(result.type, "entry");
assert.equal(result.name, "What I had for lunch");
assert.equal(result.slug, "what-i-had-for-lunch");
Expand Down
15 changes: 12 additions & 3 deletions packages/endpoint-micropub/test/unit/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ describe("endpoint-media/lib/utils", () => {
});

it("Renders path from URI template and properties", async () => {
const template = "{yyyy}/{MM}/{slug}";
const template = "{channel}/{yyyy}/{MM}/{slug}";
const properties = {
published: "2020-01-01",
channel: ["foo", "bar"],
slug: "foo",
};
const application = {
Expand All @@ -83,9 +84,17 @@ describe("endpoint-media/lib/utils", () => {
},
},
};
const result = await renderPath(template, properties, application);
const publication = {
slugSeparator: "_",
};
const result = await renderPath(
template,
properties,
application,
publication,
);

assert.match(result, /\d{4}\/\d{2}\/foo/);
assert.match(result, /foo_bar\/\d{4}\/\d{2}\/foo/);
});

it("Convert string to array if not already an array", () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/endpoint-posts/includes/post-types/mp-channel.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{ tag({
label: __("posts.form.mp-channel.label"),
items: publication.channels[property].name
}) if property }}
3 changes: 3 additions & 0 deletions packages/endpoint-posts/lib/middleware/post-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from "node:path";
import { IndiekitError } from "@indiekit/error";
import { statusTypes } from "../status-types.js";
import {
getChannelItems,
getGeoValue,
getPostName,
getPostProperties,
Expand All @@ -26,6 +27,7 @@ export const postData = {
response.locals = {
accessToken: access_token,
action: "create",
channelItems: getChannelItems(publication),
fields,
name,
postsPath: path.dirname(request.baseUrl + request.path),
Expand Down Expand Up @@ -68,6 +70,7 @@ export const postData = {
accessToken: access_token,
action: action || "create",
allDay,
channelItems: getChannelItems(publication),
draftMode: scope?.includes("draft"),
fields,
geo,
Expand Down
Loading
Loading