@rmdes/indiekit-endpoint-posts 1.0.0-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +43 -0
  2. package/assets/icon.svg +4 -0
  3. package/includes/@indiekit-endpoint-posts-syndicate.njk +21 -0
  4. package/includes/@indiekit-endpoint-posts-widget.njk +14 -0
  5. package/includes/post-types/category-field.njk +11 -0
  6. package/includes/post-types/category.njk +4 -0
  7. package/includes/post-types/content-field.njk +17 -0
  8. package/includes/post-types/content.njk +3 -0
  9. package/includes/post-types/featured-field.njk +30 -0
  10. package/includes/post-types/featured.njk +8 -0
  11. package/includes/post-types/geo-field.njk +8 -0
  12. package/includes/post-types/location-field.njk +42 -0
  13. package/includes/post-types/location.njk +12 -0
  14. package/includes/post-types/mp-channel.njk +4 -0
  15. package/includes/post-types/name-field.njk +7 -0
  16. package/includes/post-types/published.njk +4 -0
  17. package/includes/post-types/summary-field.njk +17 -0
  18. package/includes/post-types/summary.njk +4 -0
  19. package/index.js +108 -0
  20. package/lib/controllers/delete.js +52 -0
  21. package/lib/controllers/form.js +121 -0
  22. package/lib/controllers/new.js +87 -0
  23. package/lib/controllers/post.js +51 -0
  24. package/lib/controllers/posts.js +96 -0
  25. package/lib/endpoint.js +53 -0
  26. package/lib/middleware/post-data.js +98 -0
  27. package/lib/middleware/validation.js +23 -0
  28. package/lib/status-types.js +32 -0
  29. package/lib/utils.js +201 -0
  30. package/locales/de.json +127 -0
  31. package/locales/en.json +127 -0
  32. package/locales/es-419.json +127 -0
  33. package/locales/es.json +127 -0
  34. package/locales/fr.json +127 -0
  35. package/locales/hi.json +127 -0
  36. package/locales/id.json +127 -0
  37. package/locales/it.json +127 -0
  38. package/locales/nl.json +127 -0
  39. package/locales/pl.json +127 -0
  40. package/locales/pt-BR.json +127 -0
  41. package/locales/pt.json +127 -0
  42. package/locales/sr.json +127 -0
  43. package/locales/sv.json +127 -0
  44. package/locales/zh-Hans-CN.json +127 -0
  45. package/package.json +55 -0
  46. package/views/new.njk +21 -0
  47. package/views/post-delete.njk +17 -0
  48. package/views/post-form.njk +114 -0
  49. package/views/post.njk +39 -0
  50. package/views/posts.njk +13 -0
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @rmdes/indiekit-endpoint-posts
2
+
3
+ Post management endpoint for Indiekit. View posts published by your Micropub endpoint and publish new posts to it.
4
+
5
+ ## Fork Notice
6
+
7
+ This is a fork of `@indiekit/endpoint-posts` with a critical bug fix for the syndication form.
8
+
9
+ ### Bug Fixed
10
+
11
+ The syndicate form button was using `data.url` for the `source_url` value, but `data` is never defined in the template context (the controller sets `properties`, not `data`). This caused the wrong post to be syndicated when clicking the "Syndicate" button.
12
+
13
+ **PR submitted upstream:** https://github.com/getindiekit/indiekit/pull/828
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @rmdes/indiekit-endpoint-posts
19
+ ```
20
+
21
+ ### Using npm overrides (recommended)
22
+
23
+ Add to your `package.json`:
24
+
25
+ ```json
26
+ {
27
+ "overrides": {
28
+ "@indiekit/endpoint-posts": "npm:@rmdes/indiekit-endpoint-posts@^1.0.0-beta.25"
29
+ }
30
+ }
31
+ ```
32
+
33
+ This replaces the upstream package with this fork without changing your plugin configuration.
34
+
35
+ ## Options
36
+
37
+ | Option | Type | Description |
38
+ | :---------- | :------- | :-------------------------------------------------------------- |
39
+ | `mountPath` | `string` | Path to management interface. _Optional_, defaults to `/posts`. |
40
+
41
+ ## License
42
+
43
+ MIT - Original work by Paul Robert Lloyd, bug fix by Ricardo Mendes.
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
2
+ <path fill="#60C" d="M0 0h96v96H0z"/>
3
+ <path fill="#FFF" d="M76 13a4 4 0 0 1 4 4v62a4 4 0 0 1-4 4H20a4 4 0 0 1-4-4V17a4 4 0 0 1 4-4h56Zm-4 56H24v6h48v-6Zm0-12H24v6h48v-6Zm0-36H50v30h22V21ZM44 45H24v6h20v-6Zm0-12H24v6h20v-6Zm0-12H24v6h20v-6Z"/>
4
+ </svg>
@@ -0,0 +1,21 @@
1
+ <form action="{{ application._syndicationEndpointPath }}" method="post">
2
+ {{ input({
3
+ name: "access_token",
4
+ type: "hidden",
5
+ value: token
6
+ }) | indent(2) }}
7
+
8
+ {{ input({
9
+ name: "syndication[redirect_uri]",
10
+ type: "hidden",
11
+ value: redirectUri
12
+ }) | indent(2) }}
13
+
14
+ {{ button({
15
+ classes: "button--secondary button--small",
16
+ name: "syndication[source_url]",
17
+ value: properties.url,
18
+ icon: "syndicate",
19
+ text: __("posts.post.syndicate")
20
+ }) | indent(2) }}
21
+ </form>
@@ -0,0 +1,14 @@
1
+ {% if application.micropubEndpoint %}
2
+ {% call widget({
3
+ title: __("posts.create.title", ""),
4
+ image: "/assets/" + plugin.id + "/icon.svg"
5
+ }) %}
6
+ <div class="button-grid">{% for type, config in publication.postTypes | dictsort %}
7
+ {{ button({
8
+ classes: "button--secondary-on-offset button--inline",
9
+ href: application.postsEndpoint + "/create/?type=" + config.type,
10
+ text: (icon(config.type) or icon("note")) + config.name
11
+ }) }}
12
+ {% endfor %}</div>
13
+ {% endcall %}
14
+ {% endif %}
@@ -0,0 +1,11 @@
1
+ {{ tagInput({
2
+ name: "category",
3
+ value: fieldData("category").value,
4
+ label: __("posts.form.category.label"),
5
+ hint: __("posts.form.category.hint"),
6
+ optional: not field.required,
7
+ localisation: {
8
+ tag: __("posts.form.category.tag"),
9
+ tags: __("posts.form.category.label") | lower
10
+ }
11
+ }) }}
@@ -0,0 +1,4 @@
1
+ {{ tag({
2
+ label: __("posts.form.category.label"),
3
+ items: property
4
+ }) if property }}
@@ -0,0 +1,17 @@
1
+ {{ textarea({
2
+ name: "content",
3
+ value: properties.content.text or fieldData("content").value,
4
+ label: __("posts.form.content.label"),
5
+ optional: not field.required,
6
+ errorMessage: fieldData("content").errorMessage,
7
+ field: {
8
+ attributes: {
9
+ editor: true,
10
+ "editor-endpoint": application.mediaEndpoint,
11
+ "editor-id": (properties.uid or ("new-" + postType)) + "-content",
12
+ "editor-locale": application.locale,
13
+ "editor-image-upload": "false" if postType == "note" or postType == "photo",
14
+ "editor-status": "false" if not field.required
15
+ }
16
+ }
17
+ }) }}
@@ -0,0 +1,3 @@
1
+ {{ prose({
2
+ html: property.text | markdown
3
+ }) if property }}
@@ -0,0 +1,30 @@
1
+ {% call fieldset({
2
+ classes: "fieldset--group",
3
+ legend: __("posts.form.featured.label"),
4
+ optional: not field.required
5
+ }) %}
6
+ {{ fileInput({
7
+ field: {
8
+ attributes: {
9
+ endpoint: application.mediaEndpoint
10
+ }
11
+ },
12
+ name: "featured[url]",
13
+ type: "url",
14
+ value: fieldData("featured.url").value,
15
+ label: __("posts.form.media.label"),
16
+ accept: "image/*",
17
+ attributes: {
18
+ placeholder: "https://"
19
+ },
20
+ errorMessage: fieldData("featured.url").errorMessage
21
+ }) | indent(2) }}
22
+
23
+ {{ textarea({
24
+ name: "featured[alt]",
25
+ value: fieldData("featured.alt").value,
26
+ label: __("posts.form.featured.alt"),
27
+ rows: 1,
28
+ errorMessage: fieldData("featured.alt").errorMessage
29
+ }) | indent(2) }}
30
+ {% endcall %}
@@ -0,0 +1,8 @@
1
+ {% set html %}
2
+ <figure>
3
+ <img src="{{ property[0].url | url(publication.me) }}" alt="{{ property[0].alt }}">
4
+ </figure>
5
+ {% endset -%}
6
+ {{- prose({
7
+ html: html
8
+ }) if property }}
@@ -0,0 +1,8 @@
1
+ {{ geoInput({
2
+ name: "geo",
3
+ value: geo,
4
+ label: __("posts.form.geo.label"),
5
+ hint: __("posts.form.geo.hint", "50.8211, -0.1452"),
6
+ optional: not field.required,
7
+ errorMessage: fieldData("geo").errorMessage
8
+ }) }}
@@ -0,0 +1,42 @@
1
+ {% call fieldset({
2
+ classes: "fieldset--group",
3
+ legend: __("posts.form.location.label"),
4
+ optional: not field.required
5
+ }) %}
6
+ {{ input({
7
+ name: "location[name]",
8
+ value: fieldData("location.name").value,
9
+ label: __("posts.form.location.name")
10
+ }) | indent(2) }}
11
+
12
+ {{ input({
13
+ name: "location[street-address]",
14
+ value: fieldData("location.street-address").value,
15
+ label: __("posts.form.location.street-address"),
16
+ autocomplete: "address-line1"
17
+ }) | indent(2) }}
18
+
19
+ {{ input({
20
+ classes: "input--width-25",
21
+ name: "location[locality]",
22
+ value: fieldData("location.locality").value,
23
+ label: __("posts.form.location.locality"),
24
+ autocomplete: "address-level2"
25
+ }) | indent(2) }}
26
+
27
+ {{ input({
28
+ classes: "input--width-25",
29
+ name: "location[country-name]",
30
+ value: fieldData("location.country-name").value,
31
+ label: __("posts.form.location.country-name"),
32
+ autocomplete: "country-name"
33
+ }) | indent(2) }}
34
+
35
+ {{ input({
36
+ classes: "input--width-10",
37
+ name: "location[postal-code]",
38
+ value: fieldData("location.postal-code").value,
39
+ label: __("posts.form.location.postal-code"),
40
+ autocomplete: "postal-code"
41
+ }) | indent(2) }}
42
+ {% endcall %}
@@ -0,0 +1,12 @@
1
+ {% set html -%}
2
+ {{- icon("location") -}}
3
+ {{- property.name + ", " if property.name -}}
4
+ {{- property["street-address"] + ", " if property["street-address"] -}}
5
+ {{- property.locality + ", " if property.locality -}}
6
+ {{- property["country-name"] + ". " if property["country-name"] -}}
7
+ {{- property["postal-code"] if property["postal-code"] -}}
8
+ {%- endset -%}
9
+ {{- prose({
10
+ classes: "prose--caption",
11
+ html: html
12
+ }) if property }}
@@ -0,0 +1,4 @@
1
+ {{ tag({
2
+ label: __("posts.form.mp-channel.label"),
3
+ items: publication.channels[property].name
4
+ }) if property }}
@@ -0,0 +1,7 @@
1
+ {{ input({
2
+ name: "name",
3
+ value: fieldData("name").value,
4
+ label: __("posts.form.name.label"),
5
+ optional: not field.required,
6
+ errorMessage: fieldData("name").errorMessage
7
+ }) }}
@@ -0,0 +1,4 @@
1
+ <dl class="prose--caption">
2
+ <dt class="-!-visually-hidden">{{ __("posts.form.published.label") }}</dt>
3
+ <dd><time datetime="{{ property }}">{{ property | date("PPPppp", { locale: opts.locale, timeZone: application.timeZone }) }}</time></dd>
4
+ </dl>
@@ -0,0 +1,17 @@
1
+ {{ textarea({
2
+ name: "summary",
3
+ value: properties.summary,
4
+ label: __("posts.form.summary.label"),
5
+ optional: not field.required,
6
+ rows: 1,
7
+ field: {
8
+ attributes: {
9
+ editor: true,
10
+ "editor-endpoint": application.mediaEndpoint,
11
+ "editor-id": (properties.uid or ("new-" + postType)) + "-summary",
12
+ "editor-locale": application.locale,
13
+ "editor-status": "false",
14
+ "editor-toolbar": "false"
15
+ }
16
+ }
17
+ }) }}
@@ -0,0 +1,4 @@
1
+ {{ prose({
2
+ classes: "prose--subhead",
3
+ html: property
4
+ }) if property }}
package/index.js ADDED
@@ -0,0 +1,108 @@
1
+ import path from "node:path";
2
+
3
+ import { tagInputSanitizer } from "@indiekit/frontend";
4
+ import { ISO_6709_RE, isRequired } from "@indiekit/util";
5
+ import express from "express";
6
+
7
+ import { deleteController } from "./lib/controllers/delete.js";
8
+ import { formController } from "./lib/controllers/form.js";
9
+ import { newController } from "./lib/controllers/new.js";
10
+ import { postController } from "./lib/controllers/post.js";
11
+ import { postsController } from "./lib/controllers/posts.js";
12
+ import { postData } from "./lib/middleware/post-data.js";
13
+ import { validate } from "./lib/middleware/validation.js";
14
+
15
+ const defaults = { mountPath: "/posts" };
16
+ const router = express.Router();
17
+
18
+ export default class PostsEndpoint {
19
+ name = "Post management endpoint";
20
+
21
+ constructor(options = {}) {
22
+ this.options = { ...defaults, ...options };
23
+ this.mountPath = this.options.mountPath;
24
+ }
25
+
26
+ get navigationItems() {
27
+ return {
28
+ href: this.options.mountPath,
29
+ text: "posts.title",
30
+ requiresDatabase: true,
31
+ };
32
+ }
33
+
34
+ get shortcutItems() {
35
+ return {
36
+ url: path.join(this.options.mountPath, "new"),
37
+ name: "posts.create.action",
38
+ iconName: "createPost",
39
+ requiresDatabase: true,
40
+ };
41
+ }
42
+
43
+ get routes() {
44
+ router.get("/", postsController);
45
+
46
+ router.get("/new", newController.get);
47
+ router.post("/new", validate.new, newController.post);
48
+
49
+ router.get("/create", postData.create, formController.get);
50
+ router.post("/create", postData.create, validate.form, formController.post);
51
+
52
+ router.use("/:uid{/:action}", postData.read);
53
+ router.get("/:uid", postController);
54
+
55
+ router.get("/:uid/update", formController.get);
56
+ router.post("/:uid/update", validate.form, formController.post);
57
+
58
+ router.get(["/:uid/delete", "/:uid/undelete"], deleteController.get);
59
+ router.post(["/:uid/delete", "/:uid/undelete"], deleteController.post);
60
+
61
+ return router;
62
+ }
63
+
64
+ get validationSchemas() {
65
+ return {
66
+ category: {
67
+ exists: { if: (value, { req }) => req.body?.category },
68
+ tagInput: tagInputSanitizer,
69
+ isArray: true,
70
+ },
71
+ content: {
72
+ errorMessage: (value, { req }) => req.__("posts.error.content.empty"),
73
+ exists: { if: (value, { req }) => isRequired(req, "content") },
74
+ notEmpty: true,
75
+ },
76
+ "featured.url": {
77
+ errorMessage: (value, { req }) =>
78
+ req.__(`posts.error.media.empty`, "/photos/image.jpg"),
79
+ exists: { if: (value, { req }) => isRequired(req, "featured") },
80
+ notEmpty: true,
81
+ },
82
+ "featured.alt": {
83
+ errorMessage: (value, { req }) =>
84
+ req.__(`posts.error.featured-alt.empty`),
85
+ exists: { if: (value, { req }) => req.body?.featured.url },
86
+ notEmpty: true,
87
+ },
88
+ geo: {
89
+ errorMessage: (value, { req }) => req.__(`posts.error.geo.invalid`),
90
+ exists: { if: (value, { req }) => req.body?.geo },
91
+ custom: {
92
+ options: (value) => value.match(ISO_6709_RE),
93
+ },
94
+ },
95
+ name: {
96
+ errorMessage: (value, { req }) => req.__("posts.error.name.empty"),
97
+ exists: { if: (value, { req }) => isRequired(req, "name") },
98
+ notEmpty: true,
99
+ },
100
+ };
101
+ }
102
+
103
+ init(Indiekit) {
104
+ Indiekit.addEndpoint(this);
105
+ Indiekit.addPostType(false, this);
106
+ Indiekit.config.application.postsEndpoint = this.mountPath;
107
+ }
108
+ }
@@ -0,0 +1,52 @@
1
+ import { checkScope } from "@indiekit/endpoint-micropub/lib/scope.js";
2
+
3
+ import { endpoint } from "../endpoint.js";
4
+
5
+ export const deleteController = {
6
+ /**
7
+ * Confirm post to delete/undelete
8
+ * @type {import("express").RequestHandler}
9
+ */
10
+ async get(request, response) {
11
+ const { action, postName, postsPath, scope } = response.locals;
12
+
13
+ if (scope && checkScope(scope, action)) {
14
+ return response.render("post-delete", {
15
+ title: response.locals.__(`posts.${action}.title`),
16
+ parent: { text: postName },
17
+ });
18
+ }
19
+
20
+ response.redirect(postsPath);
21
+ },
22
+
23
+ /**
24
+ * Post delete/undelete action to Micropub endpoint
25
+ * @type {import("express").RequestHandler}
26
+ */
27
+ async post(request, response) {
28
+ const { micropubEndpoint } = request.app.locals.application;
29
+ const { accessToken, action, postName, properties } = response.locals;
30
+
31
+ try {
32
+ const micropubUrl = new URL(micropubEndpoint);
33
+ micropubUrl.searchParams.append("action", action);
34
+ micropubUrl.searchParams.append("url", properties.url);
35
+
36
+ const micropubResponse = await endpoint.post(
37
+ micropubUrl.href,
38
+ accessToken,
39
+ );
40
+ const message = encodeURIComponent(micropubResponse.success_description);
41
+
42
+ response.redirect(`${request.baseUrl}?success=${message}`);
43
+ } catch (error) {
44
+ response.status(error.status || 500);
45
+ response.render("post-delete", {
46
+ title: response.locals.__(`posts.${action}.title`),
47
+ parent: { text: postName },
48
+ error,
49
+ });
50
+ }
51
+ },
52
+ };
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+
3
+ import { jf2ToMf2 } from "@indiekit/endpoint-micropub/lib/mf2.js";
4
+ import { checkScope } from "@indiekit/endpoint-micropub/lib/scope.js";
5
+ import { formatLocalToZonedDate, sanitise } from "@indiekit/util";
6
+ import { validationResult } from "express-validator";
7
+
8
+ import { endpoint } from "../endpoint.js";
9
+ import { getLocationProperty } from "../utils.js";
10
+
11
+ export const formController = {
12
+ /**
13
+ * Get post to create/update
14
+ * @type {import("express").RequestHandler}
15
+ */
16
+ async get(request, response) {
17
+ const { action, postsPath, postType, name, scope } = response.locals;
18
+
19
+ if (scope && checkScope(scope, action)) {
20
+ return response.render("post-form", {
21
+ back:
22
+ action === "create"
23
+ ? {
24
+ href: `${path.join(postsPath, "new")}?type=${postType}`,
25
+ text: response.locals.__(`posts.form.back`),
26
+ }
27
+ : {
28
+ href: path.dirname(request.baseUrl + request.path),
29
+ },
30
+ title: response.locals.__(
31
+ `posts.${action}.title`,
32
+ name.toLowerCase().replace("rsvp", "RSVP"),
33
+ ),
34
+ });
35
+ }
36
+
37
+ response.redirect(postsPath);
38
+ },
39
+
40
+ /**
41
+ * Post to Micropub endpoint
42
+ * @type {import("express").RequestHandler}
43
+ */
44
+ async post(request, response) {
45
+ const { micropubEndpoint, timeZone } = request.app.locals.application;
46
+ const { accessToken, action, name, properties } = response.locals;
47
+
48
+ const errors = validationResult(request);
49
+ if (!errors.isEmpty()) {
50
+ return response.status(422).render("post-form", {
51
+ title: response.locals.__(
52
+ `posts.${action}.title`,
53
+ name.toLowerCase().replace("rsvp", "RSVP"),
54
+ ),
55
+ errors: errors.mapped(),
56
+ });
57
+ }
58
+
59
+ try {
60
+ const values = request.body;
61
+ if (values["publication-date"] === "now") {
62
+ // Remove empty local date value and let server set date
63
+ delete values.published;
64
+ } else {
65
+ // Add timezone designator to local date value
66
+ values.published = formatLocalToZonedDate(values.published, timeZone);
67
+ }
68
+
69
+ // Convert media values object to Array
70
+ for (const key of ["audio", "photo", "video"]) {
71
+ if (values[key]) {
72
+ values[key] = Object.values(values[key]);
73
+ }
74
+ }
75
+
76
+ // Derive location from location and/or geo values
77
+ if (values.location || values.geo) {
78
+ values.location = getLocationProperty(values);
79
+ }
80
+
81
+ // Delete non-MF2 properties
82
+ // @todo Use `properties` for field names whose values should be submitted
83
+ delete values["all-day"];
84
+ delete values.geo;
85
+ delete values.postType;
86
+ delete values["publication-date"];
87
+
88
+ // Easy MDE appends `image` value to formData for last image uploaded
89
+ delete values.image;
90
+
91
+ const mf2 = jf2ToMf2({ properties: sanitise(values) });
92
+
93
+ let jsonBody = mf2;
94
+ if (action === "update") {
95
+ jsonBody = {
96
+ action,
97
+ url: properties.url,
98
+ replace: mf2.properties,
99
+ };
100
+ }
101
+
102
+ const micropubResponse = await endpoint.post(
103
+ micropubEndpoint,
104
+ accessToken,
105
+ jsonBody,
106
+ );
107
+ const message = encodeURIComponent(micropubResponse.success_description);
108
+
109
+ response.redirect(`${request.baseUrl}?success=${message}`);
110
+ } catch (error) {
111
+ response.status(error.status || 500);
112
+ response.render("post-form", {
113
+ title: response.locals.__(
114
+ `posts.${action}.title`,
115
+ name.toLowerCase().replace("rsvp", "RSVP"),
116
+ ),
117
+ error,
118
+ });
119
+ }
120
+ },
121
+ };
@@ -0,0 +1,87 @@
1
+ import path from "node:path";
2
+
3
+ import { checkScope } from "@indiekit/endpoint-micropub/lib/scope.js";
4
+ import { validationResult } from "express-validator";
5
+
6
+ export const newController = {
7
+ /**
8
+ * Get new post type to create
9
+ * @type {import("express").RequestHandler}
10
+ */
11
+ async get(request, response) {
12
+ const action = "create";
13
+ const { publication } = request.app.locals;
14
+ const postsPath = path.dirname(request.baseUrl + request.path);
15
+ const postType = request.query.type;
16
+ const { scope } = request.session;
17
+
18
+ const postTypeItems = Object.values(publication.postTypes)
19
+ .toSorted((a, b) => {
20
+ return a.name.localeCompare(b.name);
21
+ })
22
+ .map((postType) => ({
23
+ label: postType.name,
24
+ value: postType.type,
25
+ }));
26
+
27
+ response.locals = {
28
+ action,
29
+ postsPath,
30
+ postType,
31
+ postTypeItems,
32
+ scope,
33
+ ...response.locals,
34
+ };
35
+
36
+ if (scope && checkScope(scope, action)) {
37
+ return response.render("new", {
38
+ back: {
39
+ href: postsPath,
40
+ },
41
+ title: response.locals.__(`posts.new.title`),
42
+ });
43
+ }
44
+
45
+ response.redirect(postsPath);
46
+ },
47
+
48
+ /**
49
+ * Redirect to post creation form for selected post type
50
+ * @type {import("express").RequestHandler}
51
+ */
52
+ async post(request, response) {
53
+ const { publication } = request.app.locals;
54
+ const postsPath = path.dirname(request.baseUrl + request.path);
55
+
56
+ const postTypeItems = Object.values(publication.postTypes)
57
+ .toSorted((a, b) => {
58
+ return a.name.localeCompare(b.name);
59
+ })
60
+ .map((postType) => ({
61
+ label: postType.name,
62
+ value: postType.type,
63
+ }));
64
+
65
+ const errors = validationResult(request);
66
+ if (!errors.isEmpty()) {
67
+ return response.status(422).render("new", {
68
+ title: response.locals.__(`posts.new.title`),
69
+ postsPath,
70
+ postTypeItems,
71
+ errors: errors.mapped(),
72
+ });
73
+ }
74
+
75
+ try {
76
+ const { type } = request.body;
77
+
78
+ response.redirect(`${request.baseUrl}/create?type=${type}`);
79
+ } catch (error) {
80
+ response.status(error.status || 500);
81
+ response.render("new", {
82
+ title: response.locals.__(`posts.new.title`),
83
+ error,
84
+ });
85
+ }
86
+ },
87
+ };