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

Refactor slugs #726

Merged
merged 12 commits into from
Mar 30, 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
4 changes: 0 additions & 4 deletions docs/configuration/tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ Tokens are available for a number of file properties, with many allowing you to
| `ss` | Second (zero-padded), for example `01` |
| `t` | UNIX epoch seconds, for example `512969520` |
| `T` | UNIX epoch milliseconds, for example `51296952000` |
| `random` | A random 5-character string, for example `w9gwi` |
| `uuid` | A [random UUID][uuid] |
| `n` | Incremental count of posts (for type) in the same day, for example `1`. This token requires a [database to be configured](https://getindiekit.com/configuration/#application-mongodburl-url). |

### Post file tokens
Expand All @@ -72,5 +70,3 @@ The following tokens are only available for media files:
| `ext` | File extension of uploaded file, for example `jpg` |
| `filename` | Slugified name of uploaded file, for example `flower_1.jpg` for a file with the original name `Flower 1.jpg`. |
| `md5` | MD5 checksum of the uploaded file, for example `be7d321488de26f2eb38834af7162164` |

[uuid]: https://www.rfc-editor.org/rfc/rfc4122.html#section-4.4
3 changes: 1 addition & 2 deletions helpers/fixtures/jf2/article-content-provided.jf2
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"type": "entry",
"name": "What I had for lunch",
"content": "> I <del>ate</del><ins>had</ins> a <cite>[cheese](https://en.wikipedia.org/wiki/Cheese)</cite> sandwich from https://cafe.example, which was > 10.\n\n-- Me, then.",
"mp-slug": "lunch"
"content": "> I <del>ate</del><ins>had</ins> a <cite>[cheese](https://en.wikipedia.org/wiki/Cheese)</cite> sandwich from https://cafe.example, which was > 10.\n\n-- Me, then."
}
6 changes: 6 additions & 0 deletions helpers/fixtures/jf2/article-slug-provided-empty.jf2
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "entry",
"name": "What I had for lunch",
"content": "I ate a *cheese* sandwich, which was nice.",
"mp-slug": ""
}
4 changes: 4 additions & 0 deletions helpers/fixtures/jf2/note-slug-missing-no-name.jf2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "entry",
"content": "I ate a cheese sandwich, which was nice."
}
5 changes: 5 additions & 0 deletions helpers/fixtures/jf2/note-slug-provided-unslugified.jf2
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "entry",
"content": "I ate a cheese sandwich, which was nice.",
"mp-slug": "Cheese sandwich"
}
5 changes: 5 additions & 0 deletions helpers/fixtures/jf2/note-slug-provided.jf2
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "entry",
"content": "I ate a cheese sandwich, which was nice.",
"mp-slug": "cheese-sandwich"
}
15 changes: 10 additions & 5 deletions packages/endpoint-media/lib/file.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { getDate } from "@indiekit/util";
import path from "node:path";
import { getDate, slugify } from "@indiekit/util";
import { fileTypeFromBuffer } from "file-type";

/**
* Derive properties from file data
* @param {object} timeZone - Application time zone
* @param {object} publication - Publication configuration
* @param {object} file - Original file object
* @param {object} timeZone - Application time zone
* @returns {Promise<object>} File properties
* @example fileData('brighton-pier.jpg') => {
* @example fileData('Brighton Pier.jpg') => {
* ext: '.jpg'
* filename: 'brighton-pier.jpg',
* 'content-type': image/jpeg,
* published: '2020-07-19T22:59:23.497Z',
* }
*/
export const getFileProperties = async (timeZone, file) => {
export const getFileProperties = async (publication, file, timeZone) => {
const { ext } = await fileTypeFromBuffer(file.data);
const published = getPublishedProperty(timeZone);

let basename = path.basename(file.name, ext);
basename = slugify(basename, publication.slugSeparator);

return {
"content-type": file.mimetype,
ext,
filename: file.name,
filename: `${basename}.${ext}`,
md5: file.md5,
published,
};
Expand Down
6 changes: 2 additions & 4 deletions packages/endpoint-media/lib/media-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export const mediaData = {
debug(`create %O`, { file });

const { hasDatabase, media, timeZone } = application;
const { me, postTypes, slugSeparator } = publication;
const { me, postTypes } = publication;

// Media properties
const properties = await getFileProperties(timeZone, file);
const properties = await getFileProperties(publication, file, timeZone);

// Get post type configuration
const type = await getMediaType(file);
Expand All @@ -44,13 +44,11 @@ export const mediaData = {
typeConfig.media.path,
properties,
application,
slugSeparator,
);
const url = await renderPath(
typeConfig.media.url || typeConfig.media.path,
properties,
application,
slugSeparator,
);
properties.url = getCanonicalUrl(url, me);

Expand Down
20 changes: 3 additions & 17 deletions packages/endpoint-media/lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { basename as getBasename } from "node:path";
import {
dateTokens,
formatDate,
getTimeZoneDesignator,
randomString,
slugify,
supplant,
} from "@indiekit/util";
import newbase60 from "newbase60";
Expand All @@ -30,10 +27,9 @@ export const getMediaProperties = (mediaData) => {
* @param {string} path - URI template path
* @param {object} properties - Media properties
* @param {object} application - Application configuration
* @param {string} separator - Slug separator
* @returns {Promise<string>} Path
*/
export const renderPath = async (path, properties, application, separator) => {
export const renderPath = async (path, properties, application) => {
const dateObject = new Date(properties.published);
const serverTimeZone = getTimeZoneDesignator();
const { locale, timeZone } = application;
Expand All @@ -56,21 +52,11 @@ export const renderPath = async (path, properties, application, separator) => {
const count = await mediaTypeCount.get(application, properties);
tokens.n = count + 1;

// Add random token
tokens.random = randomString(5)
.replace(separator, "0") // Don’t use slug separator character
.toLowerCase();

// Add UUID token
tokens.uuid = crypto.randomUUID();

// Add file extension token
tokens.ext = properties.ext;

// Add slugified file name token
let basename = getBasename(properties.filename, properties.ext);
basename = slugify(basename, separator);
tokens.filename = `${basename}.${properties.ext}`;
// Add file name token
tokens.filename = properties.filename;

// Add md5 token
tokens.md5 = properties.md5;
Expand Down
9 changes: 6 additions & 3 deletions packages/endpoint-media/test/unit/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ describe("endpoint-media/lib/file", () => {
});

it("Derives properties from file data", async () => {
const publication = {
slugSeparator: "-",
};
const file = {
data: getFixture("file-types/photo.jpg", false),
name: "photo.jpg",
name: "Photo 1.jpg",
md5: "be7d321488de26f2eb38834af7162164",
};
const result = await getFileProperties("UTC", file);
const result = await getFileProperties(publication, file, "UTC");

assert.equal(result.ext, "jpg");
assert.equal(result.filename, "photo.jpg");
assert.equal(result.filename, "photo-1.jpg");
assert.equal(result.md5, "be7d321488de26f2eb38834af7162164");
assert.equal(isValid(parseISO(result.published)), true);
});
Expand Down
12 changes: 4 additions & 8 deletions packages/endpoint-media/test/unit/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { renderPath } from "../../lib/utils.js";
describe("endpoint-media/lib/util", () => {
const properties = {
ext: "jpg",
filename: "Foo 1.jpg",
filename: "foo-1.jpg",
md5: "be7d321488de26f2eb38834af7162164",
published: "2020-01-01",
};
Expand All @@ -16,16 +16,12 @@ describe("endpoint-media/lib/util", () => {
});

it("Renders path from URI template and properties", async () => {
const dateToken = await renderPath("{yyyy}/{MM}", properties, {}, "-");
const fileToken = await renderPath("{filename}", properties, {}, "-");
const uuidToken = await renderPath("{uuid}", properties, {}, "-");
const randomToken = await renderPath("{random}", properties, {}, "-");
const md5Token = await renderPath("{md5}", properties, {}, "-");
const dateToken = await renderPath("{yyyy}/{MM}", properties, {});
const fileToken = await renderPath("{filename}", properties, {});
const md5Token = await renderPath("{md5}", properties, {});

assert.match(dateToken, /^\d{4}\/\d{2}/);
assert.match(fileToken, /^foo-1\.jpg/);
assert.match(uuidToken, /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}/);
assert.match(randomToken, /^\w{5}/);
assert.match(md5Token, /^([\da-f]{32}|[\dA-F]{32})$/);
});
});
39 changes: 33 additions & 6 deletions packages/endpoint-micropub/lib/jf2.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { getDate } from "@indiekit/util";
import { getDate, randomString, slugify } from "@indiekit/util";
import { mf2tojf2, mf2tojf2referenced } from "@paulrobertlloyd/mf2tojf2";
import { fetchReferences } from "@paulrobertlloyd/mf2tojf2";
import { markdownToHtml, htmlToMarkdown } from "./markdown.js";
import { reservedProperties } from "./reserved-properties.js";
import { decodeQueryParameter, relativeMediaPath, toArray } from "./utils.js";
import {
decodeQueryParameter,
excerptString,
relativeMediaPath,
toArray,
} from "./utils.js";

/**
* Create JF2 object from form-encoded request
Expand Down Expand Up @@ -63,7 +68,7 @@ export const mf2ToJf2 = async (body, requestReferences) => {
* @returns {object} Normalised JF2 properties
*/
export const normaliseProperties = (publication, properties, timeZone) => {
const { me } = publication;
const { me, slugSeparator } = publication;

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

Expand Down Expand Up @@ -91,9 +96,7 @@ export const normaliseProperties = (publication, properties, timeZone) => {
properties.video = getVideoProperty(properties, me);
}

if (properties["mp-slug"]) {
properties.slug = properties["mp-slug"];
}
properties.slug = getSlugProperty(properties, slugSeparator);

if (properties["mp-syndicate-to"]) {
properties["mp-syndicate-to"] = toArray(properties["mp-syndicate-to"]);
Expand Down Expand Up @@ -222,6 +225,30 @@ export const getVideoProperty = (properties, me) => {
}));
};

/**
* Get slug
* @param {object} properties - JF2 properties
* @param {string} separator - Slug separator
* @returns {string} Array containing slug value
*/
export const getSlugProperty = (properties, separator) => {
const suggested = properties["mp-slug"];
const { name } = properties;

let string;
if (suggested) {
string = suggested;
} else if (name) {
string = excerptString(name, 5);
} else {
string = randomString(5)
.replace("_", "0") // Slugify function strips any leading underscore
.replace(separator, "0"); // Don’t include slug separator character
}

return slugify(string, { separator });
};

/**
* Get `mp-syndicate-to` property
* @param {object} properties - JF2 properties
Expand Down
22 changes: 6 additions & 16 deletions packages/endpoint-micropub/lib/post-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const postData = {
debug(`create %O`, { draftMode, properties });

const { hasDatabase, posts, timeZone } = application;
const { me, postTypes, slugSeparator, syndicationTargets } = publication;
const { me, postTypes, syndicationTargets } = publication;

// Add syndication targets
const syndicateTo = getSyndicateToProperty(properties, syndicationTargets);
Expand All @@ -48,14 +48,8 @@ export const postData = {
typeConfig.post.path,
properties,
application,
slugSeparator,
);
const url = await renderPath(
typeConfig.post.url,
properties,
application,
slugSeparator,
);
const url = await renderPath(typeConfig.post.url, properties, application);
properties.url = getCanonicalUrl(url, me);

// Post status
Expand Down Expand Up @@ -108,7 +102,7 @@ export const postData = {
debug(`update ${url} %O`, { operation });

const { posts, timeZone } = application;
const { me, postTypes, slugSeparator } = publication;
const { me, postTypes } = publication;

// Read properties
let { path: _originalPath, properties } = await this.read(application, url);
Expand Down Expand Up @@ -149,13 +143,11 @@ export const postData = {
typeConfig.post.path,
properties,
application,
slugSeparator,
);
const updatedUrl = await renderPath(
typeConfig.post.url,
properties,
application,
slugSeparator,
);
properties.url = getCanonicalUrl(updatedUrl, me);

Expand Down Expand Up @@ -188,7 +180,7 @@ export const postData = {
debug(`delete ${url}`);

const { posts, timeZone } = application;
const { postTypes, slugSeparator } = publication;
const { postTypes } = publication;

// Read properties
const { properties } = await this.read(application, url);
Expand All @@ -198,7 +190,7 @@ export const postData = {

// Delete all properties, except those required for path creation
for (const key in _deletedProperties) {
if (!["mp-slug", "post-type", "published", "type", "url"].includes(key)) {
if (!["post-type", "published", "slug", "type", "url"].includes(key)) {
delete properties[key];
}
}
Expand All @@ -215,7 +207,6 @@ export const postData = {
typeConfig.post.path,
properties,
application,
slugSeparator,
);

// Update data in posts collection
Expand All @@ -240,7 +231,7 @@ export const postData = {
debug(`undelete ${url} %O`, { draftMode });

const { posts } = application;
const { postTypes, slugSeparator } = publication;
const { postTypes } = publication;

// Read deleted properties
const { _deletedProperties } = await this.read(application, url);
Expand All @@ -257,7 +248,6 @@ export const postData = {
typeConfig.post.path,
properties,
application,
slugSeparator,
);

// Post status
Expand Down
Loading