kizlo 0.7.0 → 0.8.1

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 (85) hide show
  1. package/dist/category/error.js +14 -0
  2. package/dist/category/index.js +77 -0
  3. package/dist/category/schema.js +40 -0
  4. package/dist/category/utils.js +20 -0
  5. package/dist/cli/banner.js +1 -1
  6. package/dist/cli/commands/_setup.js +342 -0
  7. package/dist/cli/commands/create.js +152 -0
  8. package/dist/cli/commands/dev.js +1 -1
  9. package/dist/cli/commands/init.js +53 -300
  10. package/dist/cli/index.js +2 -1
  11. package/dist/cli/presets/generated/templates.js +59 -0
  12. package/dist/cli/presets/nextjs.js +3 -95
  13. package/dist/cli/presets/patch.js +133 -0
  14. package/dist/cli/presets/template.js +36 -0
  15. package/dist/comment/index.js +4 -4
  16. package/dist/context.d.ts +25 -8
  17. package/dist/context.js +15 -5
  18. package/dist/email/service.d.ts +4 -3
  19. package/dist/email/service.js +4 -4
  20. package/dist/index.d.ts +15 -11
  21. package/dist/index.js +10 -4
  22. package/dist/integrations/nextjs/metadata.js +6 -3
  23. package/dist/integrations/nextjs/sitemap.d.ts +8 -7
  24. package/dist/integrations/nextjs/sitemap.js +3 -3
  25. package/dist/menu/index.js +19 -14
  26. package/dist/menu/schema.d.ts +14 -0
  27. package/dist/menu/schema.js +35 -40
  28. package/dist/menu/utils.js +23 -14
  29. package/dist/nextjs/server.d.ts +2 -1
  30. package/dist/nextjs/server.js +2 -1
  31. package/dist/package.js +1 -1
  32. package/dist/page/error.js +20 -0
  33. package/dist/page/index.js +128 -0
  34. package/dist/page/schema.js +75 -0
  35. package/dist/page/utils.js +43 -0
  36. package/dist/post/error.js +4 -14
  37. package/dist/post/index.js +17 -9
  38. package/dist/post/schema.js +37 -66
  39. package/dist/post/utils.js +27 -24
  40. package/dist/router.d.ts +1006 -185
  41. package/dist/router.js +6 -0
  42. package/dist/seo/index.js +7 -44
  43. package/dist/seo/schema.d.ts +10 -6
  44. package/dist/seo/schema.js +7 -5
  45. package/dist/seo/sitemap.d.ts +9 -1
  46. package/dist/seo/sitemap.js +17 -1
  47. package/dist/seo/types.d.ts +17 -20
  48. package/dist/seo/utils.js +9 -7
  49. package/dist/settings/index.js +27 -24
  50. package/dist/settings/service.d.ts +14 -13
  51. package/dist/settings/service.interface.d.ts +2 -2
  52. package/dist/settings/service.js +9 -9
  53. package/dist/shared/middleware.d.ts +4 -4
  54. package/dist/shared/procedure.d.ts +6 -7
  55. package/dist/tag/error.js +14 -0
  56. package/dist/tag/index.js +76 -0
  57. package/dist/tag/schema.js +38 -0
  58. package/dist/tag/utils.js +19 -0
  59. package/dist/taxonomy/service.js +48 -0
  60. package/dist/test/fixtures.d.ts +1 -1
  61. package/dist/test/fixtures.js +1 -1
  62. package/dist/webhook/index.d.ts +2 -2
  63. package/dist/wordpress/category/service.d.ts +28 -0
  64. package/dist/wordpress/category/service.js +55 -0
  65. package/dist/wordpress/category/types.d.ts +120 -0
  66. package/dist/wordpress/category/types.js +15 -0
  67. package/dist/wordpress/comment/service.d.ts +1 -1
  68. package/dist/wordpress/index.d.ts +9 -4
  69. package/dist/wordpress/menu/items/types.d.ts +7 -4
  70. package/dist/wordpress/menu/items/types.js +22 -0
  71. package/dist/wordpress/page/service.d.ts +28 -0
  72. package/dist/wordpress/page/service.js +55 -0
  73. package/dist/wordpress/page/types.d.ts +270 -0
  74. package/dist/wordpress/page/types.js +27 -0
  75. package/dist/wordpress/post/types.d.ts +9 -5
  76. package/dist/wordpress/post/types.js +41 -0
  77. package/dist/wordpress/service.d.ts +7 -1
  78. package/dist/wordpress/service.js +6 -0
  79. package/dist/wordpress/tag/service.d.ts +28 -0
  80. package/dist/wordpress/tag/service.js +55 -0
  81. package/dist/wordpress/tag/types.d.ts +113 -0
  82. package/dist/wordpress/tag/types.js +15 -0
  83. package/package.json +5 -3
  84. package/dist/service.d.ts +0 -19
  85. package/dist/service.js +0 -20
@@ -2,7 +2,7 @@ import { createProcedure } from "../shared/procedure.js";
2
2
  import { deserializeListMetadata } from "../shared/serialize.js";
3
3
  import { LIST_MENU_ITEM_ERROR_MAP } from "./errors.js";
4
4
  import { ListMenuInput, MenuGroupItemList, MenuItemList } from "./schema.js";
5
- import { buildMenuGroupItem, deserializeListMenuInput, extractSlugFromUrl } from "./utils.js";
5
+ import { buildMenuGroupItem, deserializeListMenuInput, extractPath } from "./utils.js";
6
6
  import { stringifiedMetaRecord } from "@kizlo/shared";
7
7
 
8
8
  //#region src/menu/index.ts
@@ -15,7 +15,7 @@ const MENU_ROUTER_MAP = { items: {
15
15
  query: ListMenuInput.optional(),
16
16
  errors: LIST_MENU_ITEM_ERROR_MAP
17
17
  }, async ({ context, errors, input }) => {
18
- const response = await context.service.wordpress.menus.items.list(deserializeListMenuInput(input.query));
18
+ const response = await context.wordpress.menus.items.list(deserializeListMenuInput(input.query));
19
19
  if (response.error) switch (response.error.code) {
20
20
  case "rest_post_invalid_page_number": throw errors.MENU_ITEM_INVALID_PAGE();
21
21
  case "rest_cannot_view": throw errors.MENU_ITEM_FORBIDDEN();
@@ -24,17 +24,22 @@ const MENU_ROUTER_MAP = { items: {
24
24
  throw errors.INTERNAL_SERVER_ERROR();
25
25
  }
26
26
  return {
27
- items: response.data.items.map((item) => {
28
- const href = item.object !== "custom" ? extractSlugFromUrl(item.url) : item.url;
29
- return {
30
- id: item.id,
31
- href,
32
- name: item.title.rendered,
33
- description: item.description,
34
- type: item.object,
35
- meta: stringifiedMetaRecord(item.meta ?? {})
36
- };
37
- }),
27
+ items: response.data.items.map((item) => ({
28
+ id: item.id,
29
+ parent: item.parent === 0 ? null : item.parent,
30
+ type: item.object,
31
+ objectId: item.object_id,
32
+ href: extractPath(item.url, item.object === "custom"),
33
+ name: item.title.rendered,
34
+ description: item.description,
35
+ target: item.target,
36
+ classes: item.classes.filter(Boolean),
37
+ attrTitle: item.attr_title,
38
+ xfn: item.xfn.filter(Boolean),
39
+ order: item.menu_order,
40
+ invalid: item.invalid,
41
+ meta: stringifiedMetaRecord(item.meta ?? {})
42
+ })),
38
43
  meta: deserializeListMetadata(response.data.meta)
39
44
  };
40
45
  }),
@@ -46,7 +51,7 @@ const MENU_ROUTER_MAP = { items: {
46
51
  errors: LIST_MENU_ITEM_ERROR_MAP,
47
52
  output: MenuGroupItemList
48
53
  }, async ({ context, errors, input }) => {
49
- const response = await context.service.wordpress.menus.items.list(deserializeListMenuInput(input.query));
54
+ const response = await context.wordpress.menus.items.list(deserializeListMenuInput(input.query));
50
55
  if (response.error) switch (response.error.code) {
51
56
  case "rest_post_invalid_page_number": throw errors.MENU_ITEM_INVALID_PAGE();
52
57
  case "rest_cannot_view": throw errors.MENU_ITEM_FORBIDDEN();
@@ -0,0 +1,14 @@
1
+ import { LiteralUnion, Metadata } from "@kizlo/shared";
2
+ import z from "zod/v4";
3
+
4
+ //#region src/menu/schema.d.ts
5
+ declare const MENU_TYPES: readonly ["page", "post", "category", "tag", "product", "product_cat", "product_tag", "custom"];
6
+ type MenuType = LiteralUnion<(typeof MENU_TYPES)[number], string>;
7
+ declare const MenuType: z.ZodType<MenuType>;
8
+ declare const MenuTarget: z.ZodEnum<{
9
+ "": "";
10
+ _blank: "_blank";
11
+ }>;
12
+ type MenuTarget = z.infer<typeof MenuTarget>;
13
+ //#endregion
14
+ export { MenuTarget, MenuType };
@@ -1,24 +1,25 @@
1
1
  import { ListMetadata, ListOrder } from "../shared/schema.js";
2
- import { Metadata, NumberLike, arrayable } from "@kizlo/shared";
2
+ import { WP_MENU_ITEM_ORDER_BYES, WP_MENU_ITEM_TARGETS, WP_MENU_ITEM_TAX_RELATIONS } from "../wordpress/menu/items/types.js";
3
+ import { Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
3
4
  import z from "zod/v4";
4
5
 
5
6
  //#region src/menu/schema.ts
6
- const MENU_TYPES = [
7
- "page",
8
- "post",
9
- "category",
10
- "product",
11
- "product_cat",
12
- "product_tag",
13
- "custom"
14
- ];
15
- const MenuType = z.enum(MENU_TYPES);
7
+ const MenuType = z.string();
8
+ const MenuTarget = z.enum(WP_MENU_ITEM_TARGETS);
16
9
  const MenuItem = z.object({
17
10
  id: z.number(),
11
+ parent: z.number().nullable(),
12
+ type: MenuType,
13
+ objectId: z.number(),
18
14
  href: z.string(),
19
15
  name: z.string(),
20
- type: MenuType,
21
16
  description: z.string(),
17
+ target: MenuTarget,
18
+ classes: z.array(z.string()),
19
+ attrTitle: z.string(),
20
+ xfn: z.array(z.string()),
21
+ order: z.number(),
22
+ invalid: z.boolean(),
22
23
  meta: Metadata
23
24
  });
24
25
  const MenuGroupItem = z.lazy(() => MenuItem.extend({
@@ -34,36 +35,30 @@ const MenuGroupItemList = z.object({
34
35
  items: z.array(MenuGroupItem),
35
36
  meta: ListMetadata
36
37
  });
37
- const MENU_ITEM_ORDER_BYES = [
38
- "author",
39
- "date",
40
- "id",
41
- "include",
42
- "parent",
43
- "relevance",
44
- "slug",
45
- "include_slugs",
46
- "title",
47
- "menu_order"
38
+ const MenuItemOrderBy = z.enum(WP_MENU_ITEM_ORDER_BYES).exclude(["modified"]);
39
+ const MENU_ITEM_SEARCH_COLUMNS = [
40
+ "post_title",
41
+ "post_content",
42
+ "post_excerpt"
48
43
  ];
49
- const MenuItemOrderBy = z.enum(MENU_ITEM_ORDER_BYES);
44
+ const MenuItemSearchColumn = z.enum(MENU_ITEM_SEARCH_COLUMNS);
50
45
  const ListMenuInput = z.object({
51
- page: NumberLike.optional(),
52
- perPage: NumberLike.optional(),
53
- search: z.string().optional(),
54
- after: z.string().optional(),
55
- before: z.string().optional(),
56
- exclude: arrayable(NumberLike).optional(),
57
- include: arrayable(NumberLike).optional(),
58
- offset: NumberLike.optional(),
59
- order: ListOrder.optional(),
60
- orderby: MenuItemOrderBy.optional(),
61
- searchColumns: z.array(z.string()).optional(),
62
- slug: arrayable(z.string()).optional(),
63
- taxRelation: z.enum(["AND", "OR"]).optional(),
64
- menus: arrayable(NumberLike).optional(),
65
- menusExclude: arrayable(NumberLike).optional(),
66
- menuOrder: NumberLike.optional()
46
+ page: NumberLike.pipe(z.number().int().min(1)).catch(1).optional(),
47
+ perPage: lenient(NumberLike.pipe(z.number().int().min(1).max(100))),
48
+ search: lenient(z.string()),
49
+ after: lenient(z.string()),
50
+ before: lenient(z.string()),
51
+ exclude: lenient(arrayable(NumberLike)),
52
+ include: lenient(arrayable(NumberLike)),
53
+ offset: lenient(NumberLike.pipe(z.number().int().min(0))),
54
+ order: lenient(ListOrder),
55
+ orderby: lenient(MenuItemOrderBy),
56
+ searchColumns: lenient(z.array(MenuItemSearchColumn)),
57
+ slug: lenient(arrayable(z.string())),
58
+ taxRelation: lenient(z.enum(WP_MENU_ITEM_TAX_RELATIONS)),
59
+ menus: lenient(arrayable(NumberLike)),
60
+ menusExclude: lenient(arrayable(NumberLike)),
61
+ menuOrder: lenient(NumberLike.pipe(z.number().int()))
67
62
  });
68
63
 
69
64
  //#endregion
@@ -2,6 +2,7 @@ import { stringifiedMetaRecord } from "@kizlo/shared";
2
2
 
3
3
  //#region src/menu/utils.ts
4
4
  function deserializeListMenuInput(input) {
5
+ const orderby = input?.orderby === "relevance" && !input?.search || input?.orderby === "include" && input?.include === void 0 || input?.orderby === "include_slugs" && input?.slug === void 0 ? void 0 : input?.orderby;
5
6
  return {
6
7
  context: "edit",
7
8
  after: input?.after,
@@ -13,7 +14,7 @@ function deserializeListMenuInput(input) {
13
14
  menus_exclude: input?.menusExclude,
14
15
  offset: input?.offset,
15
16
  order: input?.order,
16
- orderby: input?.orderby,
17
+ orderby,
17
18
  page: input?.page,
18
19
  per_page: input?.perPage,
19
20
  search: input?.search,
@@ -23,30 +24,38 @@ function deserializeListMenuInput(input) {
23
24
  status: "publish"
24
25
  };
25
26
  }
26
- function extractSlugFromUrl(url) {
27
+ /**
28
+ * The `href` a headless frontend links to. WP builds a non-custom item's `url` as an absolute same-site
29
+ * URL, so we return its full pathname; a custom item's `url` is passed through as authored (a relative
30
+ * path, or an absolute URL for external links). Both yield the same value for the same internal path.
31
+ */
32
+ function extractPath(url, isCustom) {
27
33
  if (!url || url.trim() === "") return "/";
28
34
  const trimmed = url.trim();
29
- let pathname;
35
+ if (isCustom) return trimmed;
30
36
  if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) try {
31
- pathname = new URL(trimmed).pathname;
37
+ return new URL(trimmed).pathname || "/";
32
38
  } catch {
33
- pathname = trimmed;
39
+ return trimmed;
34
40
  }
35
- else pathname = trimmed;
36
- const cleanPath = pathname.replace(/^\/+|\/+$/g, "");
37
- if (!cleanPath) return "/";
38
- const segments = cleanPath.split("/");
39
- return segments[segments.length - 1] ?? "/";
41
+ return trimmed;
40
42
  }
41
43
  function buildMenuGroupItem(wpItem, items) {
42
44
  const children = items.filter((item) => item.parent === wpItem.id).sort((a, b) => a.menu_order - b.menu_order).map((child) => buildMenuGroupItem(child, items));
43
- const href = wpItem.object !== "custom" ? extractSlugFromUrl(wpItem.url) : wpItem.url;
44
45
  return {
45
46
  id: wpItem.id,
46
- href,
47
+ parent: wpItem.parent === 0 ? null : wpItem.parent,
48
+ type: wpItem.object,
49
+ objectId: wpItem.object_id,
50
+ href: extractPath(wpItem.url, wpItem.object === "custom"),
47
51
  name: wpItem.title.rendered,
48
52
  description: wpItem.description,
49
- type: wpItem.object,
53
+ target: wpItem.target,
54
+ classes: wpItem.classes.filter(Boolean),
55
+ attrTitle: wpItem.attr_title,
56
+ xfn: wpItem.xfn.filter(Boolean),
57
+ order: wpItem.menu_order,
58
+ invalid: wpItem.invalid,
50
59
  items: children,
51
60
  hasItems: children.length > 0,
52
61
  meta: stringifiedMetaRecord(wpItem.meta ?? {})
@@ -54,4 +63,4 @@ function buildMenuGroupItem(wpItem, items) {
54
63
  }
55
64
 
56
65
  //#endregion
57
- export { buildMenuGroupItem, deserializeListMenuInput, extractSlugFromUrl };
66
+ export { buildMenuGroupItem, deserializeListMenuInput, extractPath };
@@ -1,4 +1,5 @@
1
1
  import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
2
+ import { createSitemapRedirectRoute } from "../seo/sitemap.js";
2
3
  import { NextRevalidateOptions, RevalidatePathFn, RevalidateTagFn, RevalidateTarget, nextRevalidation } from "../integrations/nextjs/revalidate.js";
3
4
  import { KizloServerOptions, createKizlo } from "../integrations/nextjs/kizlo.server.js";
4
5
  import { MANIFEST_CACHE_TAG, createManifestRoute } from "../integrations/nextjs/manifest.js";
@@ -6,4 +7,4 @@ import { createPageMetadata, createRootMetadata, createRootViewport } from "../i
6
7
  import { ROBOTS_CACHE_TAG, createRobotsRoute } from "../integrations/nextjs/robots.js";
7
8
  import { SITEMAP_CACHE_TAG, SITEMAP_ROUTE, createSitemapRoute } from "../integrations/nextjs/sitemap.js";
8
9
  import { createCachedTextRoute, createNextCookiesInterface, getServerBaseUrl } from "../integrations/nextjs/utils.js";
9
- export { KizloServerOptions, MANIFEST_CACHE_TAG, NextRevalidateOptions, ROBOTS_CACHE_TAG, RevalidatePathFn, RevalidateTagFn, RevalidateTarget, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRoute, getServerBaseUrl, nextRevalidation };
10
+ export { KizloServerOptions, MANIFEST_CACHE_TAG, NextRevalidateOptions, ROBOTS_CACHE_TAG, RevalidatePathFn, RevalidateTagFn, RevalidateTarget, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, nextRevalidation };
@@ -1,4 +1,5 @@
1
1
  import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
2
+ import { createSitemapRedirectRoute } from "../seo/sitemap.js";
2
3
  import { createCachedTextRoute, createNextCookiesInterface, getServerBaseUrl } from "../integrations/nextjs/utils.js";
3
4
  import { ROBOTS_CACHE_TAG, createRobotsRoute } from "../integrations/nextjs/robots.js";
4
5
  import { SITEMAP_CACHE_TAG, SITEMAP_ROUTE, createSitemapRoute } from "../integrations/nextjs/sitemap.js";
@@ -7,4 +8,4 @@ import { createKizlo } from "../integrations/nextjs/kizlo.server.js";
7
8
  import { MANIFEST_CACHE_TAG, createManifestRoute } from "../integrations/nextjs/manifest.js";
8
9
  import { createPageMetadata, createRootMetadata, createRootViewport } from "../integrations/nextjs/metadata.js";
9
10
 
10
- export { MANIFEST_CACHE_TAG, ROBOTS_CACHE_TAG, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRoute, getServerBaseUrl, nextRevalidation };
11
+ export { MANIFEST_CACHE_TAG, ROBOTS_CACHE_TAG, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, nextRevalidation };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.7.0";
2
+ var version = "0.8.1";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -0,0 +1,20 @@
1
+ import { defineErrorMap } from "../shared/error.js";
2
+
3
+ //#region src/page/error.ts
4
+ const GET_PAGE_ERROR_MAP = defineErrorMap({
5
+ PAGE_NOT_FOUND: {
6
+ status: 404,
7
+ message: "Page not found"
8
+ },
9
+ PAGE_PASSWORD_INVALID: {
10
+ status: 400,
11
+ message: "Invalid page password."
12
+ }
13
+ });
14
+ const LIST_PAGE_ERROR_MAP = defineErrorMap({ PAGE_INVALID_PAGE: {
15
+ status: 400,
16
+ message: "The requested page does not exist."
17
+ } });
18
+
19
+ //#endregion
20
+ export { GET_PAGE_ERROR_MAP, LIST_PAGE_ERROR_MAP };
@@ -0,0 +1,128 @@
1
+ import { compare } from "../shared/crypto.js";
2
+ import { createProcedure } from "../shared/procedure.js";
3
+ import { parseIdentifier } from "../shared/identifier.js";
4
+ import { deserializeListMetadata } from "../shared/serialize.js";
5
+ import { getPostTypeService } from "../post-type/service.js";
6
+ import { GET_PAGE_ERROR_MAP, LIST_PAGE_ERROR_MAP } from "./error.js";
7
+ import { ListPageInput, Page, PageList, RetrievePageInput } from "./schema.js";
8
+ import { deserializePage } from "./utils.js";
9
+
10
+ //#region src/page/index.ts
11
+ const PAGE_ROUTER_MAP = {
12
+ get: createProcedure({
13
+ scope: "api",
14
+ method: "GET",
15
+ path: "/pages/{identifier}",
16
+ params: RetrievePageInput.pick({ identifier: true }),
17
+ query: RetrievePageInput.pick({
18
+ password: true,
19
+ previewToken: true
20
+ }).optional(),
21
+ output: Page,
22
+ errors: GET_PAGE_ERROR_MAP
23
+ }, async ({ input, context, errors }) => {
24
+ const postType = getPostTypeService("page", context.wordpress);
25
+ if (input.query?.previewToken) {
26
+ const result = await context.verifyPreviewToken(input.query.previewToken);
27
+ if (!result) throw errors.PAGE_NOT_FOUND();
28
+ const response$1 = await postType.get({
29
+ type: "id",
30
+ value: result.id
31
+ });
32
+ if (response$1.error) switch (response$1.error.code) {
33
+ case "invalid_post_type":
34
+ case "post_type_not_found":
35
+ case "rest_post_invalid_id": throw errors.PAGE_NOT_FOUND();
36
+ case "rest_post_incorrect_password": throw errors.PAGE_PASSWORD_INVALID();
37
+ default:
38
+ context.logger.error("Get page preview unhandled error", response$1.error, {
39
+ id: result.id,
40
+ code: response$1.error.code
41
+ });
42
+ throw errors.INTERNAL_SERVER_ERROR();
43
+ }
44
+ return deserializePage({
45
+ ...response$1.data,
46
+ password: ""
47
+ }, { preview: true });
48
+ }
49
+ const identifier = parseIdentifier(input.params.identifier);
50
+ if (!identifier) throw errors.PAGE_NOT_FOUND();
51
+ const response = await postType.get(identifier);
52
+ if (response.error) switch (response.error.code) {
53
+ case "invalid_post_type":
54
+ case "post_type_not_found":
55
+ case "rest_post_invalid_id": throw errors.PAGE_NOT_FOUND();
56
+ case "rest_post_incorrect_password": throw errors.PAGE_PASSWORD_INVALID();
57
+ default:
58
+ context.logger.error("Get page unhandled error", response.error, {
59
+ identifier,
60
+ code: response.error.code
61
+ });
62
+ throw errors.INTERNAL_SERVER_ERROR();
63
+ }
64
+ const data = response.data;
65
+ if (data.status !== "publish") throw errors.PAGE_NOT_FOUND();
66
+ if (input.query?.password && data.password) {
67
+ if (!await compare(input.query.password, data.password)) throw errors.PAGE_PASSWORD_INVALID();
68
+ return deserializePage({
69
+ ...data,
70
+ password: ""
71
+ });
72
+ }
73
+ return deserializePage(data);
74
+ }),
75
+ list: createProcedure({
76
+ scope: "api",
77
+ method: "GET",
78
+ path: "/pages",
79
+ query: ListPageInput.optional(),
80
+ output: PageList,
81
+ errors: LIST_PAGE_ERROR_MAP
82
+ }, async ({ input, context, errors }) => {
83
+ const q = input.query;
84
+ const postType = getPostTypeService("page", context.wordpress);
85
+ const orderby = q?.orderby === "relevance" && !q?.search || q?.orderby === "include" && q?.include === void 0 ? void 0 : q?.orderby;
86
+ const response = await postType.list({
87
+ context: "edit",
88
+ status: "publish",
89
+ after: input.query?.after,
90
+ author: input.query?.author,
91
+ author_exclude: input.query?.authorExclude,
92
+ before: input.query?.before,
93
+ modified_after: input.query?.modifiedAfter,
94
+ modified_before: input.query?.modifiedBefore,
95
+ exclude: input.query?.exclude,
96
+ include: input.query?.include,
97
+ menu_order: input.query?.menuOrder,
98
+ offset: input.query?.offset,
99
+ order: input.query?.order,
100
+ orderby,
101
+ page: input.query?.page,
102
+ parent: input.query?.parent,
103
+ parent_exclude: input.query?.parentExclude,
104
+ per_page: input.query?.perPage,
105
+ search: input.query?.search,
106
+ search_columns: input.query?.searchColumns,
107
+ slug: input.query?.slug
108
+ });
109
+ if (response.error) switch (response.error.code) {
110
+ case "rest_post_invalid_page_number": throw errors.PAGE_INVALID_PAGE();
111
+ default:
112
+ context.logger.error("List pages unhandled error", response.error, { code: response.error.code });
113
+ throw errors.INTERNAL_SERVER_ERROR();
114
+ }
115
+ const list = context.wordpress.resolveList({
116
+ data: response.data,
117
+ headers: response.headers,
118
+ searchParams: input.query
119
+ });
120
+ return {
121
+ meta: deserializeListMetadata(list.meta),
122
+ items: list.items.map((item) => deserializePage(item))
123
+ };
124
+ })
125
+ };
126
+
127
+ //#endregion
128
+ export { PAGE_ROUTER_MAP };
@@ -0,0 +1,75 @@
1
+ import { IdentifierInput, ListMetadata, ListOrder, Media } from "../shared/schema.js";
2
+ import { WP_PAGE_ORDER_BYES, WP_PAGE_STATUSES } from "../wordpress/page/types.js";
3
+ import { Seo } from "../seo/schema.js";
4
+ import { Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
5
+ import z from "zod/v4";
6
+
7
+ //#region src/page/schema.ts
8
+ const PageStatus = z.enum(WP_PAGE_STATUSES).exclude(["trash"]);
9
+ const PageAuthorRef = z.object({
10
+ id: z.number(),
11
+ name: z.string(),
12
+ slug: z.string(),
13
+ avatar: Media.nullable()
14
+ });
15
+ const Page = z.object({
16
+ id: z.number(),
17
+ slug: z.string(),
18
+ url: z.string().nullable(),
19
+ parent: z.number().nullable(),
20
+ menuOrder: z.number(),
21
+ template: z.string(),
22
+ title: z.string().nullable(),
23
+ content: z.string().nullable(),
24
+ excerpt: z.string().nullable(),
25
+ commentsEnabled: z.boolean(),
26
+ protected: z.boolean(),
27
+ preview: z.boolean(),
28
+ status: PageStatus,
29
+ author: PageAuthorRef.nullable(),
30
+ featuredMedia: Media.nullable(),
31
+ seo: Seo.nullable(),
32
+ createdAt: z.number(),
33
+ updatedAt: z.number(),
34
+ meta: Metadata
35
+ });
36
+ const RetrievePageInput = z.object({
37
+ identifier: IdentifierInput,
38
+ password: z.string().optional(),
39
+ previewToken: z.string().optional()
40
+ });
41
+ const PageList = z.object({
42
+ items: z.array(Page),
43
+ meta: ListMetadata
44
+ });
45
+ const PageOrderBy = z.enum(WP_PAGE_ORDER_BYES);
46
+ const PAGE_SEARCH_COLUMNS = [
47
+ "post_title",
48
+ "post_content",
49
+ "post_excerpt"
50
+ ];
51
+ const PageSearchColumn = z.enum(PAGE_SEARCH_COLUMNS);
52
+ const ListPageInput = z.object({
53
+ page: NumberLike.pipe(z.number().int().min(1)).catch(1).optional(),
54
+ perPage: lenient(NumberLike.pipe(z.number().int().min(1).max(100))),
55
+ search: lenient(z.string()),
56
+ after: lenient(z.string()),
57
+ before: lenient(z.string()),
58
+ modifiedAfter: lenient(z.string()),
59
+ modifiedBefore: lenient(z.string()),
60
+ author: lenient(arrayable(NumberLike)),
61
+ authorExclude: lenient(arrayable(NumberLike)),
62
+ exclude: lenient(arrayable(NumberLike)),
63
+ include: lenient(arrayable(NumberLike)),
64
+ offset: lenient(NumberLike.pipe(z.number().int().min(0))),
65
+ order: lenient(ListOrder),
66
+ orderby: lenient(PageOrderBy),
67
+ searchColumns: lenient(z.array(PageSearchColumn)),
68
+ slug: lenient(arrayable(z.string())),
69
+ parent: lenient(arrayable(NumberLike)),
70
+ parentExclude: lenient(arrayable(NumberLike)),
71
+ menuOrder: lenient(NumberLike)
72
+ });
73
+
74
+ //#endregion
75
+ export { ListPageInput, Page, PageList, RetrievePageInput };
@@ -0,0 +1,43 @@
1
+ import { deserializeSeo } from "../seo/utils.js";
2
+ import { resolveWpTimestamp, stringifiedMetaRecord } from "@kizlo/shared";
3
+
4
+ //#region src/page/utils.ts
5
+ function deserializePage(data, options) {
6
+ const locked = data.password.length > 0;
7
+ const kizlo = data.kizlo;
8
+ const title = data.title.rendered;
9
+ return {
10
+ id: data.id,
11
+ url: kizlo.url ?? null,
12
+ parent: data.parent || null,
13
+ menuOrder: data.menu_order ?? 0,
14
+ template: data.template ?? "",
15
+ title: title.length > 0 ? title : null,
16
+ content: locked ? null : data.content?.rendered ?? null,
17
+ excerpt: locked ? null : data.excerpt?.rendered ?? null,
18
+ protected: locked,
19
+ preview: options?.preview ?? false,
20
+ status: data.status === "trash" ? "draft" : data.status,
21
+ featuredMedia: kizlo.featured_media ?? null,
22
+ commentsEnabled: data.comment_status === "open",
23
+ slug: data.slug,
24
+ author: kizlo.author ? {
25
+ id: kizlo.author.id,
26
+ name: kizlo.author.name,
27
+ slug: kizlo.author.slug,
28
+ avatar: kizlo.author.avatar_url ? {
29
+ id: 0,
30
+ alt: kizlo.author.name,
31
+ name: kizlo.author.name,
32
+ src: kizlo.author.avatar_url
33
+ } : null
34
+ } : null,
35
+ seo: kizlo.seo ? deserializeSeo(kizlo.seo) : null,
36
+ createdAt: resolveWpTimestamp(data.date_gmt) ?? resolveWpTimestamp(data.modified_gmt) ?? 0,
37
+ updatedAt: resolveWpTimestamp(data.modified_gmt) ?? 0,
38
+ meta: stringifiedMetaRecord(data.meta ?? {})
39
+ };
40
+ }
41
+
42
+ //#endregion
43
+ export { deserializePage };
@@ -11,20 +11,10 @@ const GET_POST_ERROR_MAP = defineErrorMap({
11
11
  message: "Invalid post password."
12
12
  }
13
13
  });
14
- const LIST_POST_ERROR_MAP = defineErrorMap({
15
- POST_INVALID_PAGE: {
16
- status: 400,
17
- message: "The requested page does not exist."
18
- },
19
- POST_SEARCH_REQUIRED: {
20
- status: 400,
21
- message: "A search term is required."
22
- },
23
- POST_ORDERBY_INCLUDE_MISSING: {
24
- status: 400,
25
- message: "Ordering by 'include' requires the include parameter."
26
- }
27
- });
14
+ const LIST_POST_ERROR_MAP = defineErrorMap({ POST_INVALID_PAGE: {
15
+ status: 400,
16
+ message: "The requested page does not exist."
17
+ } });
28
18
 
29
19
  //#endregion
30
20
  export { GET_POST_ERROR_MAP, LIST_POST_ERROR_MAP };