@websline/cms-view-utils 1.7.2 → 1.8.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/package.json +47 -42
- package/src/integration/cmsRoutes.js +62 -0
- package/src/middleware/createCmsMiddleware.js +13 -1
- package/src/middleware/withRequestFilter.js +20 -14
- package/src/routes/cmsProxy.js +5 -0
- package/src/routes/llms.txt.js +5 -0
- package/src/routes/robots.txt.js +5 -0
- package/src/routes/sitemap.xml.js +5 -0
- package/src/server.js +1 -0
- package/src/shared/routes.js +7 -0
package/package.json
CHANGED
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
2
|
+
"name": "@websline/cms-view-utils",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"src",
|
|
7
|
+
"dist",
|
|
8
|
+
"!src/**/*.test.js"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./server": "./src/server.js",
|
|
13
|
+
"./integration": "./src/integration/cmsRoutes.js",
|
|
14
|
+
"./routes/*": "./src/routes/*",
|
|
15
|
+
"./components/*": "./src/components/*",
|
|
16
|
+
"./preview-bridge": "./dist/previewBridge.global.js",
|
|
17
|
+
"./edit-scroll": "./dist/editScroll.global.js",
|
|
18
|
+
"./dist/*": "./dist/*"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"jsonwebtoken": "^9.0.3",
|
|
22
|
+
"tailwind-merge": "^3.7.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/compat": "^2.1.1",
|
|
26
|
+
"@eslint/js": "^10.0.1",
|
|
27
|
+
"eslint": "^10.11.0",
|
|
28
|
+
"eslint-config-prettier": "^10.1.8",
|
|
29
|
+
"eslint-plugin-svelte": "^3.23.0",
|
|
30
|
+
"globals": "^17.12.0",
|
|
31
|
+
"prettier": "^3.9.8",
|
|
32
|
+
"prettier-plugin-svelte": "^4.1.1",
|
|
33
|
+
"prettier-plugin-tailwindcss": "^0.8.1",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"vitest": "^5.0.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"astro": ">=5",
|
|
39
|
+
"svelte": "^5.0.0"
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"format": "prettier --write .",
|
|
45
|
+
"lint": "prettier --check . && eslint .",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CMS_ROUTE_PATTERN } from "../shared/routes.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Astro integration that ships the CMS-backed routes every website needs, so a
|
|
5
|
+
* site repository does not have to re-declare them. A project file of the same
|
|
6
|
+
* pattern wins over the injected route; `exclude` makes that intent explicit.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const INJECTED_ROUTES = {
|
|
10
|
+
"/robots.txt": "@websline/cms-view-utils/routes/robots.txt.js",
|
|
11
|
+
"/sitemap.xml": "@websline/cms-view-utils/routes/sitemap.xml.js",
|
|
12
|
+
"/llms.txt": "@websline/cms-view-utils/routes/llms.txt.js",
|
|
13
|
+
"/api/cms/[...path]": "@websline/cms-view-utils/routes/cmsProxy.js",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} [options]
|
|
18
|
+
* @param {string[]} [options.exclude] - route patterns the project provides itself
|
|
19
|
+
* @param {string} [options.cmsRoutePattern] - route pattern of the CMS catch-all page
|
|
20
|
+
*/
|
|
21
|
+
const cmsRoutes = (options = {}) => {
|
|
22
|
+
const { exclude = [], cmsRoutePattern = CMS_ROUTE_PATTERN } = options;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: "@websline/cms-view-utils",
|
|
26
|
+
hooks: {
|
|
27
|
+
"astro:config:setup": ({ injectRoute }) => {
|
|
28
|
+
for (const [pattern, entrypoint] of Object.entries(INJECTED_ROUTES)) {
|
|
29
|
+
if (exclude.includes(pattern)) continue;
|
|
30
|
+
|
|
31
|
+
injectRoute({ pattern, entrypoint });
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"astro:routes:resolved": ({ routes, logger }) => {
|
|
35
|
+
const hasCmsRoute = routes.some((route) => route.pattern === cmsRoutePattern);
|
|
36
|
+
|
|
37
|
+
if (!hasCmsRoute) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`@websline/cms-view-utils: no route matches "${cmsRoutePattern}". ` +
|
|
40
|
+
"The CMS middleware only runs for that route, so every page would " +
|
|
41
|
+
"render without CMS data. Add src/pages/[...path].astro or pass " +
|
|
42
|
+
"cmsRoutePattern to cmsRoutes() and createCmsMiddleware().",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const pattern of Object.keys(INJECTED_ROUTES)) {
|
|
47
|
+
const origins = routes
|
|
48
|
+
.filter((route) => route.pattern === pattern)
|
|
49
|
+
.map((route) => route.origin);
|
|
50
|
+
|
|
51
|
+
if (origins.includes("project") && origins.includes("external")) {
|
|
52
|
+
logger.info(
|
|
53
|
+
`Project route "${pattern}" overrides the injected one. Pass it in cmsRoutes({ exclude }) to make that explicit.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export { cmsRoutes };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { withRequestFilter } from "./withRequestFilter.js";
|
|
2
2
|
import { withCmsFetch } from "./withCmsFetch.js";
|
|
3
|
+
import { CMS_ROUTE_PATTERN } from "../shared/routes.js";
|
|
4
|
+
import { HttpError } from "../shared/errors.js";
|
|
3
5
|
|
|
4
6
|
const defaultErrorHandler = (context, error, wrap) => {
|
|
5
7
|
const status = error?.status ?? 500;
|
|
@@ -11,7 +13,12 @@ const defaultErrorHandler = (context, error, wrap) => {
|
|
|
11
13
|
/**
|
|
12
14
|
* Composes the standard CMS middleware pipeline.
|
|
13
15
|
*
|
|
16
|
+
* The CMS catch-all route renders CMS data and nothing else, so a request that
|
|
17
|
+
* reaches it without running the pipeline is answered 404 instead of rendering
|
|
18
|
+
* an empty page.
|
|
19
|
+
*
|
|
14
20
|
* @param {object} [options]
|
|
21
|
+
* @param {string} [options.cmsRoutePattern] - route pattern of the CMS catch-all page
|
|
15
22
|
* @param {(context: import("astro").APIContext) => boolean} [options.shouldSkip] - filter that decides if request bypasses the pipeline
|
|
16
23
|
* @param {(context: import("astro").APIContext) => Promise<void>} [options.fetch] - CMS fetch step
|
|
17
24
|
* @param {(context: import("astro").APIContext, handler: () => any) => Promise<Response>} [options.paraglide] - i18n wrapper (optional)
|
|
@@ -21,7 +28,8 @@ const defaultErrorHandler = (context, error, wrap) => {
|
|
|
21
28
|
*/
|
|
22
29
|
const createCmsMiddleware = (options = {}) => {
|
|
23
30
|
const {
|
|
24
|
-
|
|
31
|
+
cmsRoutePattern = CMS_ROUTE_PATTERN,
|
|
32
|
+
shouldSkip = withRequestFilter({ cmsRoutePattern }),
|
|
25
33
|
fetch = withCmsFetch(),
|
|
26
34
|
paraglide,
|
|
27
35
|
beforeFetch,
|
|
@@ -33,6 +41,10 @@ const createCmsMiddleware = (options = {}) => {
|
|
|
33
41
|
|
|
34
42
|
return async (context, next) => {
|
|
35
43
|
if (shouldSkip(context)) {
|
|
44
|
+
if (context.routePattern === cmsRoutePattern) {
|
|
45
|
+
return onError(context, new HttpError(404, "Not Found"), wrap);
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
return next();
|
|
37
49
|
}
|
|
38
50
|
|
|
@@ -1,33 +1,39 @@
|
|
|
1
|
+
import { CMS_ROUTE_PATTERN } from "../shared/routes.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Default request filter —
|
|
3
|
-
*
|
|
4
|
+
* Default request filter — the CMS pipeline runs for the catch-all route only.
|
|
5
|
+
*
|
|
6
|
+
* Every other route Astro matched is its own (a project page, an endpoint, an
|
|
7
|
+
* injected route) and must not be fed CMS data. Static files never get here:
|
|
8
|
+
* Astro drops them before route matching, and the server in front serves them.
|
|
9
|
+
*
|
|
10
|
+
* Requires `context.routePattern` (Astro 5 or newer).
|
|
4
11
|
*
|
|
5
12
|
* @param {object} [options]
|
|
13
|
+
* @param {string} [options.cmsRoutePattern] - route pattern of the CMS catch-all page
|
|
6
14
|
* @param {string[]} [options.additionalExcludePaths] - extra path prefixes to skip
|
|
7
15
|
* @param {(context: import("astro").APIContext) => boolean} [options.customFilter] - additional skip predicate
|
|
8
16
|
*/
|
|
9
|
-
|
|
10
17
|
const withRequestFilter = (options = {}) => {
|
|
11
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
cmsRoutePattern = CMS_ROUTE_PATTERN,
|
|
20
|
+
additionalExcludePaths = [],
|
|
21
|
+
customFilter,
|
|
22
|
+
} = options;
|
|
12
23
|
|
|
13
24
|
return (context) => {
|
|
14
|
-
const { request, url } = context;
|
|
25
|
+
const { request, url, routePattern } = context;
|
|
15
26
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const hasFileExtension = /\.[a-zA-Z0-9]+$/.test(url.pathname);
|
|
27
|
+
const isNonGet = request.method !== "GET";
|
|
28
|
+
const isOwnAstroRoute = routePattern !== cmsRoutePattern;
|
|
19
29
|
const isExcludedPath = additionalExcludePaths.some((p) =>
|
|
20
30
|
url.pathname.startsWith(p),
|
|
21
31
|
);
|
|
22
|
-
const isAstroApiCall = url.pathname.startsWith("/api/");
|
|
23
32
|
|
|
24
33
|
return (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
isErrorRoute ||
|
|
28
|
-
hasFileExtension ||
|
|
34
|
+
isNonGet ||
|
|
35
|
+
isOwnAstroRoute ||
|
|
29
36
|
isExcludedPath ||
|
|
30
|
-
isAstroApiCall ||
|
|
31
37
|
customFilter?.(context) === true
|
|
32
38
|
);
|
|
33
39
|
};
|
package/src/server.js
CHANGED
|
@@ -8,3 +8,4 @@ export { createSitemapHandler } from "./handlers/sitemapHandler.js";
|
|
|
8
8
|
export { createRobotsHandler } from "./handlers/robotsHandler.js";
|
|
9
9
|
export { createLlmsTxtHandler } from "./handlers/llmsTxtHandler.js";
|
|
10
10
|
export { createCmsProxyHandler } from "./cms/proxyToCms.js";
|
|
11
|
+
export { CMS_ROUTE_PATTERN } from "./shared/routes.js";
|