@easy-cms/plugin-redirects 0.18.0
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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +284 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kanawoot K.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @easy-cms/plugin-redirects
|
|
2
|
+
|
|
3
|
+
Redirects for [Easy CMS](https://maritonx.github.io/easy-cms/): a Redirects list under Settings
|
|
4
|
+
in the admin, and automatic redirects when a published page's address changes.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { redirectsPlugin } from '@easy-cms/plugin-redirects'
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
// …
|
|
11
|
+
plugins: [
|
|
12
|
+
redirectsPlugin({
|
|
13
|
+
collections: ['posts'],
|
|
14
|
+
url: ({ doc }) => (doc.slug ? `/posts/${doc.slug}` : null),
|
|
15
|
+
}),
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { resolveRedirect } from '@easy-cms/plugin-redirects'
|
|
22
|
+
|
|
23
|
+
// In your app's middleware
|
|
24
|
+
const redirect = await resolveRedirect(cms, new URL(request.url))
|
|
25
|
+
if (redirect) return Response.redirect(new URL(redirect.location, request.url), redirect.status)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See [Redirects](https://maritonx.github.io/easy-cms/guide/redirects).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Plugin } from '@easy-cms/core';
|
|
2
|
+
|
|
3
|
+
type Doc$1 = Record<string, unknown>;
|
|
4
|
+
interface UrlArgs {
|
|
5
|
+
readonly collection: string;
|
|
6
|
+
/** The document, as read for visitors. */
|
|
7
|
+
readonly doc: Doc$1;
|
|
8
|
+
/** The content locale, or `null` without localization. */
|
|
9
|
+
readonly locale: string | null;
|
|
10
|
+
}
|
|
11
|
+
interface RedirectsPluginOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Collections a redirect can point to, as a document instead of a fixed address. The
|
|
14
|
+
* redirect follows the document when its address changes, and pages in these collections
|
|
15
|
+
* get a redirect automatically when their address changes (see `autoRedirect`).
|
|
16
|
+
*/
|
|
17
|
+
readonly collections?: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* A document's address, e.g. `({ doc }) => \`/posts/${doc.slug}\``. Needed with `collections`;
|
|
20
|
+
* share one function with the SEO plugin's `generateURL`.
|
|
21
|
+
*/
|
|
22
|
+
readonly url?: (args: UrlArgs) => string | null | undefined | Promise<string | null | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* When a published page's address changes (a new slug), add a redirect from the old address.
|
|
25
|
+
* Default true for all `collections`; or a list of them.
|
|
26
|
+
*/
|
|
27
|
+
readonly autoRedirect?: boolean | readonly string[];
|
|
28
|
+
/** Slug of the redirects collection. Default `redirects`. */
|
|
29
|
+
readonly slug?: string;
|
|
30
|
+
/**
|
|
31
|
+
* How long a server keeps the redirects in memory, in ms. Changes clear it at once on the
|
|
32
|
+
* server that made them; other servers (serverless instances) catch up within this time.
|
|
33
|
+
* Default 60000.
|
|
34
|
+
*/
|
|
35
|
+
readonly cacheTTL?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A `redirects` collection under Settings in the admin, `resolveRedirect()` for your app's
|
|
39
|
+
* middleware, and automatic redirects when a page's address changes.
|
|
40
|
+
*/
|
|
41
|
+
declare function redirectsPlugin(options?: RedirectsPluginOptions): Plugin;
|
|
42
|
+
|
|
43
|
+
type Doc = Record<string, unknown>;
|
|
44
|
+
/** The part of the Local API the helpers use; a structural type. */
|
|
45
|
+
interface RedirectsCMS {
|
|
46
|
+
readonly config: {
|
|
47
|
+
readonly endpoints: readonly {
|
|
48
|
+
readonly handler: unknown;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
find(collection: string, options: Record<string, unknown>): Promise<{
|
|
52
|
+
docs: Doc[];
|
|
53
|
+
hasNextPage: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
findById(collection: string, id: string | number, options?: Record<string, unknown>): Promise<Doc | null>;
|
|
56
|
+
}
|
|
57
|
+
interface Redirect {
|
|
58
|
+
/** Where to send the visitor: a path on the site, or a full URL. */
|
|
59
|
+
readonly location: string;
|
|
60
|
+
readonly status: 301 | 302 | 307 | 308;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A path as redirects store it: the pathname only (no query or fragment), no trailing slash.
|
|
64
|
+
* `null` when it isn't a path.
|
|
65
|
+
*/
|
|
66
|
+
declare function normalizePath(input: string): string | null;
|
|
67
|
+
/** Forgets the cached redirects, so the next request reads them again. */
|
|
68
|
+
declare function clearRedirects(cms: object): void;
|
|
69
|
+
/**
|
|
70
|
+
* The redirect for a request's path, or `null`. Pass the request's URL to keep its query
|
|
71
|
+
* string. Redirects are read once and kept in memory (see `cacheTTL`), so calling this on every
|
|
72
|
+
* request is cheap.
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* const redirect = await resolveRedirect(cms, new URL(request.url))
|
|
76
|
+
* if (redirect) return Response.redirect(new URL(redirect.location, request.url), redirect.status)
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare function resolveRedirect(cms: RedirectsCMS, input: string | URL): Promise<Redirect | null>;
|
|
80
|
+
|
|
81
|
+
declare const STATUSES: readonly [301, 302, 307, 308];
|
|
82
|
+
type RedirectStatus = (typeof STATUSES)[number];
|
|
83
|
+
|
|
84
|
+
export { type Redirect, type RedirectStatus, type RedirectsPluginOptions, STATUSES, type UrlArgs, clearRedirects, normalizePath, redirectsPlugin, resolveRedirect };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/shared.ts
|
|
2
|
+
var REDIRECTS_SOURCE = /* @__PURE__ */ Symbol.for("easy-cms.plugin-redirects.source");
|
|
3
|
+
var STATUSES = [301, 302, 307, 308];
|
|
4
|
+
var targetField = (collection) => `to_${collection.replace(/-/g, "_")}`;
|
|
5
|
+
|
|
6
|
+
// src/resolve.ts
|
|
7
|
+
function normalizePath(input) {
|
|
8
|
+
const value = input.trim();
|
|
9
|
+
if (!value) return null;
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(value, "http://site.invalid");
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!/^(\/|https?:\/\/)/i.test(value)) return null;
|
|
17
|
+
return url.pathname.replace(/\/+$/, "") || "/";
|
|
18
|
+
}
|
|
19
|
+
var CACHE = /* @__PURE__ */ Symbol.for("easy-cms.plugin-redirects.cache");
|
|
20
|
+
function clearRedirects(cms) {
|
|
21
|
+
delete cms[CACHE];
|
|
22
|
+
}
|
|
23
|
+
async function resolveRedirect(cms, input) {
|
|
24
|
+
const url = typeof input === "string" ? safeUrl(input) : input;
|
|
25
|
+
if (!url) return null;
|
|
26
|
+
const path = normalizePath(url.pathname);
|
|
27
|
+
if (!path) return null;
|
|
28
|
+
const found = (await redirects(cms)).get(path);
|
|
29
|
+
if (!found) return null;
|
|
30
|
+
const location = url.search && !found.location.includes("?") ? found.location + url.search : found.location;
|
|
31
|
+
return { location, status: found.status };
|
|
32
|
+
}
|
|
33
|
+
function safeUrl(input) {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(input, "http://site.invalid");
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function findSource(cms) {
|
|
41
|
+
for (const endpoint of cms.config.endpoints) {
|
|
42
|
+
const source = endpoint.handler[REDIRECTS_SOURCE];
|
|
43
|
+
if (source) return source;
|
|
44
|
+
}
|
|
45
|
+
throw new Error("resolveRedirect: add redirectsPlugin() to the plugins in your Easy CMS config");
|
|
46
|
+
}
|
|
47
|
+
function redirects(cms) {
|
|
48
|
+
const source = findSource(cms);
|
|
49
|
+
const holder = cms;
|
|
50
|
+
const cached = holder[CACHE];
|
|
51
|
+
if (cached && Date.now() - cached.at < source.cacheTTL) return cached.map;
|
|
52
|
+
const map = load(cms, source);
|
|
53
|
+
holder[CACHE] = { at: Date.now(), map };
|
|
54
|
+
map.catch(() => {
|
|
55
|
+
if (holder[CACHE]?.map === map) delete holder[CACHE];
|
|
56
|
+
});
|
|
57
|
+
return map;
|
|
58
|
+
}
|
|
59
|
+
async function load(cms, source) {
|
|
60
|
+
const map = /* @__PURE__ */ new Map();
|
|
61
|
+
for (let page = 1; ; page++) {
|
|
62
|
+
const result = await cms.find(source.slug, { depth: 0, sort: "id", limit: 500, page });
|
|
63
|
+
for (const doc of result.docs) {
|
|
64
|
+
const from = typeof doc.from === "string" ? normalizePath(doc.from) : null;
|
|
65
|
+
if (!from) continue;
|
|
66
|
+
const location = await target(cms, source, doc);
|
|
67
|
+
if (!location || normalizePath(location) === from) continue;
|
|
68
|
+
const status = Number(doc.type);
|
|
69
|
+
map.set(from, {
|
|
70
|
+
location,
|
|
71
|
+
status: STATUSES.includes(status) ? status : 301
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (!result.hasNextPage) break;
|
|
75
|
+
}
|
|
76
|
+
return map;
|
|
77
|
+
}
|
|
78
|
+
async function target(cms, source, doc) {
|
|
79
|
+
if (typeof doc.to === "string" && doc.to.trim()) return doc.to.trim();
|
|
80
|
+
for (const collection of source.collections) {
|
|
81
|
+
const ref = doc[targetField(collection)];
|
|
82
|
+
const id = typeof ref === "object" && ref !== null ? ref.id : ref;
|
|
83
|
+
if (id === null || id === void 0) continue;
|
|
84
|
+
const locale = typeof doc.locale === "string" ? doc.locale : null;
|
|
85
|
+
const page = await cms.findById(collection, id, {
|
|
86
|
+
depth: 0,
|
|
87
|
+
...locale ? { locale } : {}
|
|
88
|
+
});
|
|
89
|
+
if (!page || page.status !== void 0 && page.status !== "published") return null;
|
|
90
|
+
const url = await source.url?.({ collection, doc: page, locale });
|
|
91
|
+
return typeof url === "string" && url.trim() ? url.trim() : null;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/plugin.ts
|
|
97
|
+
function redirectsPlugin(options = {}) {
|
|
98
|
+
return (config) => {
|
|
99
|
+
const slug = options.slug ?? "redirects";
|
|
100
|
+
const collections = options.collections ?? [];
|
|
101
|
+
const missing = collections.filter((s) => !config.collections?.some((c) => c.slug === s));
|
|
102
|
+
if (missing.length > 0)
|
|
103
|
+
throw new Error(
|
|
104
|
+
`redirectsPlugin: unknown collection ${missing.map((s) => `"${s}"`).join(", ")}`
|
|
105
|
+
);
|
|
106
|
+
if (collections.length > 0 && !options.url)
|
|
107
|
+
throw new Error(
|
|
108
|
+
"redirectsPlugin: set `url` to give the address of documents in `collections`"
|
|
109
|
+
);
|
|
110
|
+
if (config.collections?.some((c) => c.slug === slug))
|
|
111
|
+
throw new Error(`redirectsPlugin: there is already a collection "${slug}"; set \`slug\``);
|
|
112
|
+
const locales = config.localization?.locales ?? [];
|
|
113
|
+
const auto = options.autoRedirect === false ? [] : options.autoRedirect === true || options.autoRedirect === void 0 ? collections : options.autoRedirect.filter((s) => collections.includes(s));
|
|
114
|
+
const urlOf = async (cms, collection, id, locale) => {
|
|
115
|
+
const doc = await cms.findById(collection, id, {
|
|
116
|
+
depth: 0,
|
|
117
|
+
...locale ? { locale } : {}
|
|
118
|
+
});
|
|
119
|
+
if (!doc || doc.status !== void 0 && doc.status !== "published") return null;
|
|
120
|
+
const url = await options.url?.({ collection, doc, locale });
|
|
121
|
+
return typeof url === "string" && url.trim() ? normalizePath(url) : null;
|
|
122
|
+
};
|
|
123
|
+
const localeList = locales.length > 0 ? locales : [null];
|
|
124
|
+
const source = {
|
|
125
|
+
slug,
|
|
126
|
+
collections,
|
|
127
|
+
url: options.url,
|
|
128
|
+
cacheTTL: options.cacheTTL ?? 6e4
|
|
129
|
+
};
|
|
130
|
+
const targets = collections.map((collection) => {
|
|
131
|
+
const label = config.collections?.find((c) => c.slug === collection)?.labels?.singular;
|
|
132
|
+
return {
|
|
133
|
+
name: targetField(collection),
|
|
134
|
+
type: "relationship",
|
|
135
|
+
to: collection,
|
|
136
|
+
label: label ?? collection
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
const redirectFields = [
|
|
140
|
+
{
|
|
141
|
+
name: "from",
|
|
142
|
+
type: "text",
|
|
143
|
+
required: true,
|
|
144
|
+
unique: true,
|
|
145
|
+
label: { en: "From", th: "\u0E08\u0E32\u0E01" },
|
|
146
|
+
validate: (value) => value === null || value === void 0 || normalizePath(String(value)) !== null ? true : "must be a path like /old-page"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "to",
|
|
150
|
+
type: "text",
|
|
151
|
+
label: { en: "To (address)", th: "\u0E44\u0E1B\u0E17\u0E35\u0E48 (\u0E17\u0E35\u0E48\u0E2D\u0E22\u0E39\u0E48)" },
|
|
152
|
+
validate: (value, { data }) => {
|
|
153
|
+
const set = [value, ...collections.map((c) => data[targetField(c)])].filter(
|
|
154
|
+
(v) => v !== null && v !== void 0 && v !== ""
|
|
155
|
+
);
|
|
156
|
+
if (set.length === 0)
|
|
157
|
+
return collections.length > 0 ? "give an address or pick a document" : "give an address, e.g. /new-page or https://example.com";
|
|
158
|
+
if (set.length > 1) return "give an address or a document, not both";
|
|
159
|
+
if (typeof value === "string" && value && !/^(\/|https?:\/\/)/.test(value.trim()))
|
|
160
|
+
return "must start with / or http(s)://";
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
...targets,
|
|
165
|
+
...locales.length > 0 && collections.length > 0 ? [
|
|
166
|
+
{
|
|
167
|
+
name: "locale",
|
|
168
|
+
type: "select",
|
|
169
|
+
options: [...locales],
|
|
170
|
+
label: { en: "Document locale", th: "\u0E20\u0E32\u0E29\u0E32\u0E02\u0E2D\u0E07\u0E40\u0E2D\u0E01\u0E2A\u0E32\u0E23" }
|
|
171
|
+
}
|
|
172
|
+
] : [],
|
|
173
|
+
{
|
|
174
|
+
name: "type",
|
|
175
|
+
type: "select",
|
|
176
|
+
options: STATUSES.map(String),
|
|
177
|
+
defaultValue: "301",
|
|
178
|
+
required: true,
|
|
179
|
+
label: { en: "Type", th: "\u0E0A\u0E19\u0E34\u0E14" },
|
|
180
|
+
position: "sidebar"
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
const clear = ({ cms }) => clearRedirects(cms);
|
|
184
|
+
const redirects2 = {
|
|
185
|
+
slug,
|
|
186
|
+
labels: {
|
|
187
|
+
singular: { en: "Redirect", th: "\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E40\u0E2A\u0E49\u0E19\u0E17\u0E32\u0E07" },
|
|
188
|
+
plural: { en: "Redirects", th: "\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E40\u0E2A\u0E49\u0E19\u0E17\u0E32\u0E07" }
|
|
189
|
+
},
|
|
190
|
+
icon: "link",
|
|
191
|
+
useAsTitle: "from",
|
|
192
|
+
editIn: "drawer",
|
|
193
|
+
admin: { group: "settings" },
|
|
194
|
+
fields: redirectFields,
|
|
195
|
+
hooks: {
|
|
196
|
+
beforeChange: [
|
|
197
|
+
({ data }) => typeof data.from === "string" ? { ...data, from: normalizePath(data.from) ?? data.from } : data
|
|
198
|
+
],
|
|
199
|
+
afterChange: [clear],
|
|
200
|
+
afterDelete: [clear]
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const before = /* @__PURE__ */ new Map();
|
|
204
|
+
const remember = async ({ data, operation, originalDoc, cms, slug: s }) => {
|
|
205
|
+
if (operation === "update" && originalDoc?.id !== void 0)
|
|
206
|
+
before.set(
|
|
207
|
+
`${s}:${originalDoc.id}`,
|
|
208
|
+
await Promise.all(localeList.map((l) => urlOf(cms, s, originalDoc.id, l)))
|
|
209
|
+
);
|
|
210
|
+
return data;
|
|
211
|
+
};
|
|
212
|
+
const redirectOld = async ({ doc, cms, slug: s }) => {
|
|
213
|
+
clearRedirects(cms);
|
|
214
|
+
const key = `${s}:${doc.id}`;
|
|
215
|
+
const old = before.get(key);
|
|
216
|
+
before.delete(key);
|
|
217
|
+
if (!old) return;
|
|
218
|
+
const now = await Promise.all(localeList.map((l) => urlOf(cms, s, doc.id, l)));
|
|
219
|
+
const done = /* @__PURE__ */ new Set();
|
|
220
|
+
for (const [i, locale] of localeList.entries()) {
|
|
221
|
+
const from = old[i];
|
|
222
|
+
const to = now[i];
|
|
223
|
+
if (!from || !to || from === to || done.has(from)) continue;
|
|
224
|
+
done.add(from);
|
|
225
|
+
await addRedirect(cms, { from, collection: s, id: doc.id, locale, current: to });
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const addRedirect = async (cms, args) => {
|
|
229
|
+
const loops = await cms.find(slug, { where: { from: { equals: args.current } }, limit: 100 });
|
|
230
|
+
for (const loop of loops.docs) await cms.delete(slug, loop.id);
|
|
231
|
+
const data = {
|
|
232
|
+
from: args.from,
|
|
233
|
+
to: null,
|
|
234
|
+
[targetField(args.collection)]: args.id,
|
|
235
|
+
...locales.length > 0 ? { locale: args.locale } : {},
|
|
236
|
+
type: "301"
|
|
237
|
+
};
|
|
238
|
+
const existing = await cms.find(slug, { where: { from: { equals: args.from } }, limit: 1 });
|
|
239
|
+
if (existing.docs[0]) await cms.update(slug, existing.docs[0].id, data);
|
|
240
|
+
else await cms.create(slug, data);
|
|
241
|
+
};
|
|
242
|
+
const withAuto = (c) => {
|
|
243
|
+
if (!collections.includes(c.slug)) return c;
|
|
244
|
+
const hooks = { ...c.hooks };
|
|
245
|
+
if (auto.includes(c.slug)) {
|
|
246
|
+
hooks.beforeChange = [...hooks.beforeChange ?? [], remember];
|
|
247
|
+
hooks.afterChange = [...hooks.afterChange ?? [], redirectOld];
|
|
248
|
+
} else {
|
|
249
|
+
hooks.afterChange = [...hooks.afterChange ?? [], clear];
|
|
250
|
+
}
|
|
251
|
+
hooks.afterDelete = [...hooks.afterDelete ?? [], clear];
|
|
252
|
+
return { ...c, hooks };
|
|
253
|
+
};
|
|
254
|
+
const endpoint = {
|
|
255
|
+
path: "/resolve-redirect",
|
|
256
|
+
method: "get",
|
|
257
|
+
// Lets `resolveRedirect(cms)` find these options from the resolved config.
|
|
258
|
+
handler: Object.assign(
|
|
259
|
+
async ({ url, cms }) => {
|
|
260
|
+
const path = url.searchParams.get("path");
|
|
261
|
+
if (!path)
|
|
262
|
+
return Response.json({ errors: [{ message: "path is required" }] }, { status: 400 });
|
|
263
|
+
const redirect = await resolveRedirect(cms, path);
|
|
264
|
+
if (!redirect)
|
|
265
|
+
return Response.json({ errors: [{ message: "No redirect" }] }, { status: 404 });
|
|
266
|
+
return Response.json(redirect, { headers: { "cache-control": "public, max-age=60" } });
|
|
267
|
+
},
|
|
268
|
+
{ [REDIRECTS_SOURCE]: source }
|
|
269
|
+
)
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
...config,
|
|
273
|
+
collections: [...(config.collections ?? []).map(withAuto), redirects2],
|
|
274
|
+
endpoints: [...config.endpoints ?? [], endpoint]
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export {
|
|
279
|
+
STATUSES,
|
|
280
|
+
clearRedirects,
|
|
281
|
+
normalizePath,
|
|
282
|
+
redirectsPlugin,
|
|
283
|
+
resolveRedirect
|
|
284
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@easy-cms/plugin-redirects",
|
|
3
|
+
"version": "0.18.0",
|
|
4
|
+
"description": "Redirects for Easy CMS: 301s managed in the admin, automatic redirects when a page's address changes",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@easy-cms/core": "^0.18.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@easy-cms/core": "^0.18.0",
|
|
23
|
+
"@easy-cms/db-sqlite": "^0.18.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.12"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/maritonx/easy-cms.git",
|
|
34
|
+
"directory": "packages/plugin-redirects"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"easy-cms",
|
|
38
|
+
"easy-cms-plugin",
|
|
39
|
+
"redirects",
|
|
40
|
+
"301",
|
|
41
|
+
"seo"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
}
|
|
48
|
+
}
|