@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 CHANGED
@@ -1,43 +1,48 @@
1
1
  {
2
- "name": "@websline/cms-view-utils",
3
- "version": "1.7.2",
4
- "type": "module",
5
- "files": [
6
- "src",
7
- "dist"
8
- ],
9
- "exports": {
10
- ".": "./src/index.js",
11
- "./server": "./src/server.js",
12
- "./components/*": "./src/components/*",
13
- "./preview-bridge": "./dist/previewBridge.global.js",
14
- "./edit-scroll": "./dist/editScroll.global.js",
15
- "./dist/*": "./dist/*"
16
- },
17
- "dependencies": {
18
- "jsonwebtoken": "^9.0.3",
19
- "tailwind-merge": "^3.6.0"
20
- },
21
- "devDependencies": {
22
- "@eslint/compat": "^2.1.0",
23
- "@eslint/js": "^10.0.1",
24
- "eslint": "^10.9.0",
25
- "eslint-config-prettier": "^10.1.8",
26
- "eslint-plugin-svelte": "^3.23.0",
27
- "globals": "^17.11.0",
28
- "prettier": "^3.9.6",
29
- "prettier-plugin-svelte": "^4.1.1",
30
- "prettier-plugin-tailwindcss": "^0.8.1",
31
- "tsup": "^8.5.1"
32
- },
33
- "peerDependencies": {
34
- "svelte": "^5.0.0"
35
- },
36
- "scripts": {
37
- "build": "tsup",
38
- "prepublishOnly": "pnpm build",
39
- "format": "prettier --write .",
40
- "lint": "prettier --check . && eslint ."
41
- },
42
- "sideEffects": false
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
- shouldSkip = withRequestFilter(),
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 — skips internal Astro routes, error routes
3
- * and asset paths (by file extension).
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 { additionalExcludePaths = [], customFilter } = options;
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 isAstroInternal = url.pathname.startsWith("/_astro");
17
- const isErrorRoute = url.pathname.startsWith("/errors/");
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
- request.method !== "GET" ||
26
- isAstroInternal ||
27
- isErrorRoute ||
28
- hasFileExtension ||
34
+ isNonGet ||
35
+ isOwnAstroRoute ||
29
36
  isExcludedPath ||
30
- isAstroApiCall ||
31
37
  customFilter?.(context) === true
32
38
  );
33
39
  };
@@ -0,0 +1,5 @@
1
+ import { createCmsProxyHandler } from "../cms/proxyToCms.js";
2
+
3
+ export const prerender = false;
4
+
5
+ export const GET = createCmsProxyHandler();
@@ -0,0 +1,5 @@
1
+ import { createLlmsTxtHandler } from "../handlers/llmsTxtHandler.js";
2
+
3
+ export const prerender = false;
4
+
5
+ export const GET = createLlmsTxtHandler();
@@ -0,0 +1,5 @@
1
+ import { createRobotsHandler } from "../handlers/robotsHandler.js";
2
+
3
+ export const prerender = false;
4
+
5
+ export const GET = createRobotsHandler();
@@ -0,0 +1,5 @@
1
+ import { createSitemapHandler } from "../handlers/sitemapHandler.js";
2
+
3
+ export const prerender = false;
4
+
5
+ export const GET = createSitemapHandler();
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";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The route pattern of the CMS catch-all page. The middleware pipeline and the
3
+ * injected routes both key off it, so it lives in one place.
4
+ */
5
+ const CMS_ROUTE_PATTERN = "/[...path]";
6
+
7
+ export { CMS_ROUTE_PATTERN };