@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.
- package/README.md +43 -0
- package/assets/icon.svg +4 -0
- package/includes/@indiekit-endpoint-posts-syndicate.njk +21 -0
- package/includes/@indiekit-endpoint-posts-widget.njk +14 -0
- package/includes/post-types/category-field.njk +11 -0
- package/includes/post-types/category.njk +4 -0
- package/includes/post-types/content-field.njk +17 -0
- package/includes/post-types/content.njk +3 -0
- package/includes/post-types/featured-field.njk +30 -0
- package/includes/post-types/featured.njk +8 -0
- package/includes/post-types/geo-field.njk +8 -0
- package/includes/post-types/location-field.njk +42 -0
- package/includes/post-types/location.njk +12 -0
- package/includes/post-types/mp-channel.njk +4 -0
- package/includes/post-types/name-field.njk +7 -0
- package/includes/post-types/published.njk +4 -0
- package/includes/post-types/summary-field.njk +17 -0
- package/includes/post-types/summary.njk +4 -0
- package/index.js +108 -0
- package/lib/controllers/delete.js +52 -0
- package/lib/controllers/form.js +121 -0
- package/lib/controllers/new.js +87 -0
- package/lib/controllers/post.js +51 -0
- package/lib/controllers/posts.js +96 -0
- package/lib/endpoint.js +53 -0
- package/lib/middleware/post-data.js +98 -0
- package/lib/middleware/validation.js +23 -0
- package/lib/status-types.js +32 -0
- package/lib/utils.js +201 -0
- package/locales/de.json +127 -0
- package/locales/en.json +127 -0
- package/locales/es-419.json +127 -0
- package/locales/es.json +127 -0
- package/locales/fr.json +127 -0
- package/locales/hi.json +127 -0
- package/locales/id.json +127 -0
- package/locales/it.json +127 -0
- package/locales/nl.json +127 -0
- package/locales/pl.json +127 -0
- package/locales/pt-BR.json +127 -0
- package/locales/pt.json +127 -0
- package/locales/sr.json +127 -0
- package/locales/sv.json +127 -0
- package/locales/zh-Hans-CN.json +127 -0
- package/package.json +55 -0
- package/views/new.njk +21 -0
- package/views/post-delete.njk +17 -0
- package/views/post-form.njk +114 -0
- package/views/post.njk +39 -0
- 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.
|
package/assets/icon.svg
ADDED
|
@@ -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,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,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,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,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
|
+
}) }}
|
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
|
+
};
|