kizlo 0.2.1 → 0.3.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.
Files changed (47) hide show
  1. package/README.md +7 -11
  2. package/dist/adapters/captcha.js +13 -5
  3. package/dist/cli/commands/dev.js +1 -1
  4. package/dist/cli/commands/init.js +51 -36
  5. package/dist/cli/commands/test.js +4 -1
  6. package/dist/cli/presets/base.js +17 -9
  7. package/dist/cli/presets/nextjs.js +61 -14
  8. package/dist/cli/wp/dev.js +1 -1
  9. package/dist/cli/wp/phpunit.js +104 -0
  10. package/dist/context.js +2 -2
  11. package/dist/index.d.ts +6 -4
  12. package/dist/index.js +4 -3
  13. package/dist/integrations/nextjs/kizlo.server.d.ts +5 -6
  14. package/dist/integrations/nextjs/kizlo.server.js +7 -5
  15. package/dist/integrations/nextjs/revalidate.d.ts +28 -0
  16. package/dist/integrations/nextjs/revalidate.js +49 -0
  17. package/dist/integrations/nextjs/robots.d.ts +7 -0
  18. package/dist/integrations/nextjs/robots.js +23 -0
  19. package/dist/integrations/nextjs/seo.d.ts +7 -0
  20. package/dist/integrations/nextjs/seo.js +41 -0
  21. package/dist/integrations/nextjs/sitemap.d.ts +33 -0
  22. package/dist/integrations/nextjs/sitemap.js +31 -0
  23. package/dist/nextjs/server.d.ts +3 -1
  24. package/dist/nextjs/server.js +3 -1
  25. package/dist/nextjs.d.ts +2 -1
  26. package/dist/nextjs.js +2 -1
  27. package/dist/package.js +1 -1
  28. package/dist/post/index.js +2 -2
  29. package/dist/router.d.ts +71 -13
  30. package/dist/seo/index.js +71 -6
  31. package/dist/seo/robots.js +18 -0
  32. package/dist/seo/schema.d.ts +15 -1
  33. package/dist/seo/schema.js +5 -1
  34. package/dist/seo/sitemap.d.ts +39 -0
  35. package/dist/seo/sitemap.js +64 -0
  36. package/dist/seo/types.d.ts +5 -1
  37. package/dist/seo/utils.d.ts +6 -0
  38. package/dist/seo/utils.js +33 -1
  39. package/dist/settings/service.interface.d.ts +1 -0
  40. package/dist/shared/crypto.d.ts +11 -8
  41. package/dist/shared/crypto.js +26 -9
  42. package/dist/shared/error.d.ts +3 -3
  43. package/dist/shared/procedure.d.ts +10 -10
  44. package/dist/webhook/schema.d.ts +2 -9
  45. package/dist/webhook/schema.js +5 -16
  46. package/dist/wordpress/service.js +4 -1
  47. package/package.json +2 -2
@@ -1,12 +1,14 @@
1
1
  import { Kizlo, resolveKizloConfig } from "../../kizlo.js";
2
+ import { nextRevalidation } from "./revalidate.js";
2
3
 
3
4
  //#region src/integrations/nextjs/kizlo.server.ts
4
- /**
5
- * Creates a Kizlo server for Next.js — same as the base `createKizlo`, but reads
6
- * the public base URL from `NEXT_PUBLIC_KIZLO_BACKEND_URL`.
7
- */
8
5
  function createKizlo(options) {
9
- return new Kizlo(resolveKizloConfig(options, { baseUrlEnvKey: "NEXT_PUBLIC_KIZLO_BACKEND_URL" }));
6
+ const revalidate = options?.revalidate;
7
+ const extensions = [...revalidate === false ? [] : [nextRevalidation(typeof revalidate === "object" ? revalidate : void 0)], ...options?.extensions ?? []];
8
+ return new Kizlo(resolveKizloConfig({
9
+ ...options,
10
+ extensions
11
+ }, { baseUrlEnvKey: "NEXT_PUBLIC_KIZLO_BACKEND_URL" }));
10
12
  }
11
13
 
12
14
  //#endregion
@@ -0,0 +1,28 @@
1
+ import { KizloEvent } from "../../webhook/schema.js";
2
+ import "../../index.js";
3
+ import { Pathname } from "@kizlo/shared";
4
+
5
+ //#region src/integrations/nextjs/revalidate.d.ts
6
+ type RevalidatePathFn = (path: string, type?: "layout" | "page") => void;
7
+ type RevalidateTagFn = (tag: string, profile: string | {
8
+ expire?: number;
9
+ }) => void;
10
+ type RevalidateTarget = Pathname | {
11
+ path: Pathname;
12
+ type?: "layout" | "page";
13
+ };
14
+ interface NextRevalidateOptions {
15
+ revalidateTag?: RevalidateTagFn;
16
+ revalidatePath?: RevalidatePathFn;
17
+ /** Return extra paths to revalidate for a given event, on top of the event's own URL. */
18
+ paths?: (event: KizloEvent) => Pathname[] | Promise<Pathname[]>;
19
+ /**
20
+ * How to revalidate the sitemap. Defaults to the route `createSitemapRoute` serves
21
+ * (`/sitemaps/[sitemap]`, revalidated as a `layout` so the whole on-demand subtree
22
+ * refreshes at once). Override this only if you serve sitemaps from your own route(s)
23
+ * instead of `createSitemapRoute`; pass `false` to skip sitemap revalidation.
24
+ */
25
+ sitemap?: false | RevalidateTarget | RevalidateTarget[];
26
+ }
27
+ //#endregion
28
+ export { NextRevalidateOptions };
@@ -0,0 +1,49 @@
1
+ import { POST_EVENT_TYPES, TERM_EVENT_TYPES } from "../../webhook/schema.js";
2
+ import { createEventHandler } from "../../webhook/index.js";
3
+ import { createExtension } from "../../shared/extension.js";
4
+ import { ROBOTS_CACHE_TAG } from "./robots.js";
5
+ import { SITEMAP_ROUTE } from "./sitemap.js";
6
+
7
+ //#region src/integrations/nextjs/revalidate.ts
8
+ const DEFAULT_SITEMAP_TARGET = {
9
+ path: SITEMAP_ROUTE,
10
+ type: "layout"
11
+ };
12
+ const CONTENT_EVENT_TYPES = new Set([...POST_EVENT_TYPES, ...TERM_EVENT_TYPES]);
13
+ function nextRevalidation(options) {
14
+ return createExtension({
15
+ id: "nextjs-revalidation",
16
+ init: () => ({ events: [createEventHandler(async (event) => {
17
+ if (!event) return;
18
+ const revalidatePath = options?.revalidatePath ?? (await import("next/cache")).revalidatePath;
19
+ const revalidateTag = options?.revalidateTag ?? (await import("next/cache")).revalidateTag;
20
+ const paths = [
21
+ ...await Promise.resolve(options?.paths?.(event) ?? []),
22
+ ...event.data?.url ? [event.data.url] : [],
23
+ "/post"
24
+ ];
25
+ for (const path of normalizePaths(paths)) revalidatePath(path);
26
+ if (event.type === "settings.saved") revalidateTag(ROBOTS_CACHE_TAG, { expire: 0 });
27
+ if (CONTENT_EVENT_TYPES.has(event.type)) for (const target of resolveSitemapTargets(options?.sitemap)) revalidatePath(normalizePath(target.path), target.type);
28
+ })] })
29
+ });
30
+ }
31
+ function resolveSitemapTargets(option) {
32
+ if (option === false) return [];
33
+ return (option === void 0 ? [DEFAULT_SITEMAP_TARGET] : Array.isArray(option) ? option : [option]).map((target) => typeof target === "string" ? { path: target } : target);
34
+ }
35
+ function normalizePaths(paths) {
36
+ return Array.from(new Set((Array.isArray(paths) ? paths : paths ? [paths] : []).map(normalizePath).filter(Boolean)));
37
+ }
38
+ function normalizePath(path) {
39
+ try {
40
+ return normalizePath(new URL(path).pathname);
41
+ } catch {
42
+ const trimmed = path.trim();
43
+ if (!trimmed) return "";
44
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
45
+ }
46
+ }
47
+
48
+ //#endregion
49
+ export { nextRevalidation };
@@ -0,0 +1,7 @@
1
+ import { S2SClient } from "../../kizlo.js";
2
+
3
+ //#region src/integrations/nextjs/robots.d.ts
4
+
5
+ declare function createRobotsRoute(client: S2SClient<[]>): (request: Request) => Promise<Response>;
6
+ //#endregion
7
+ export { createRobotsRoute };
@@ -0,0 +1,23 @@
1
+ import { createRobotsRoute } from "../../seo/robots.js";
2
+
3
+ //#region src/integrations/nextjs/robots.ts
4
+ /**
5
+ * Cache tag for the cached robots.txt body. The Next revalidation extension calls
6
+ * `revalidateTag(ROBOTS_CACHE_TAG)` on every webhook event so the route refreshes
7
+ * on-demand instead of on every request.
8
+ */
9
+ const ROBOTS_CACHE_TAG = "kizlo:robots";
10
+ function createRobotsRoute$1(client) {
11
+ const handler = createRobotsRoute(client);
12
+ let getBody;
13
+ return async function GET(request) {
14
+ if (!getBody) {
15
+ const { unstable_cache } = await import("next/cache");
16
+ getBody = unstable_cache(async () => (await handler(request)).text(), [ROBOTS_CACHE_TAG], { tags: [ROBOTS_CACHE_TAG] });
17
+ }
18
+ return new Response(await getBody(), { headers: { "Content-Type": "text/plain; charset=utf-8" } });
19
+ };
20
+ }
21
+
22
+ //#endregion
23
+ export { ROBOTS_CACHE_TAG, createRobotsRoute$1 as createRobotsRoute };
@@ -0,0 +1,7 @@
1
+ import { SeoHead } from "../../seo/schema.js";
2
+ import { Metadata } from "next";
3
+
4
+ //#region src/integrations/nextjs/seo.d.ts
5
+ declare function toMetadata(head: SeoHead): Metadata;
6
+ //#endregion
7
+ export { toMetadata };
@@ -0,0 +1,41 @@
1
+ //#region src/integrations/nextjs/seo.ts
2
+ function toMetadata(head) {
3
+ const robots = [
4
+ head.robots.index,
5
+ head.robots.follow,
6
+ head.robots.maxSnippet,
7
+ head.robots.maxImagePreview,
8
+ head.robots.maxVideoPreview
9
+ ].join(", ");
10
+ return {
11
+ title: { absolute: head.title },
12
+ description: head.og.description || void 0,
13
+ alternates: { canonical: head.canonical },
14
+ robots,
15
+ openGraph: {
16
+ type: head.og.type,
17
+ title: head.og.title,
18
+ description: head.og.description || void 0,
19
+ url: head.og.url,
20
+ siteName: head.og.siteName,
21
+ locale: head.og.locale,
22
+ images: head.og.image ? [{
23
+ url: head.og.image.url,
24
+ width: head.og.image.width,
25
+ height: head.og.image.height,
26
+ alt: head.og.image.alt ?? void 0
27
+ }] : void 0
28
+ },
29
+ twitter: {
30
+ card: head.twitter.card,
31
+ title: head.twitter.title,
32
+ description: head.twitter.description || void 0,
33
+ site: head.twitter.site ?? void 0,
34
+ creator: head.twitter.creator ?? void 0,
35
+ images: head.twitter.image ? [head.twitter.image] : void 0
36
+ }
37
+ };
38
+ }
39
+
40
+ //#endregion
41
+ export { toMetadata };
@@ -0,0 +1,33 @@
1
+ import { S2SClient } from "../../kizlo.js";
2
+ import { CreateSitemapRouteOptions } from "../../seo/sitemap.js";
3
+
4
+ //#region src/integrations/nextjs/sitemap.d.ts
5
+
6
+ /**
7
+ * The Next route the sitemap handler is mounted at: a single dynamic segment named
8
+ * `[sitemap]`. It is fixed (not configurable) so that one `revalidatePath(SITEMAP_ROUTE,
9
+ * "layout")` refreshes the index and every generated sitemap at once. A `layout`
10
+ * revalidation (not `page`) is required because the route has no `generateStaticParams`,
11
+ * so its slugs are generated on-demand and only a subtree invalidation reaches them.
12
+ * `[sitemap]` is Next's file-routing convention, which is why it lives in this integration
13
+ * and not in core.
14
+ */
15
+ declare const SITEMAP_ROUTE: "/sitemaps/[sitemap]";
16
+ /** Next's dynamic-route context. `params` is a promise in the App Router. */
17
+ interface NextRouteContext {
18
+ params?: {
19
+ sitemap?: string;
20
+ } | Promise<{
21
+ sitemap?: string;
22
+ }>;
23
+ }
24
+ /**
25
+ * Next wrapper around the framework-agnostic {@link createCoreSitemapRoute}. It serves the
26
+ * same sitemap index and collection pages, and additionally asserts the route is mounted at
27
+ * {@link SITEMAP_ROUTE}: if the dynamic segment is not named `[sitemap]`, `params.sitemap`
28
+ * is missing and a wrong mount would silently break `revalidatePath(SITEMAP_ROUTE, "layout")`,
29
+ * so it throws loudly on the first request instead.
30
+ */
31
+ declare function createSitemapRoute(client: S2SClient<[]>, options?: CreateSitemapRouteOptions): (request: Request, ctx?: NextRouteContext) => Promise<Response>;
32
+ //#endregion
33
+ export { SITEMAP_ROUTE, createSitemapRoute };
@@ -0,0 +1,31 @@
1
+ import { SITEMAP_BASE, createSitemapRoute } from "../../seo/sitemap.js";
2
+
3
+ //#region src/integrations/nextjs/sitemap.ts
4
+ /**
5
+ * The Next route the sitemap handler is mounted at: a single dynamic segment named
6
+ * `[sitemap]`. It is fixed (not configurable) so that one `revalidatePath(SITEMAP_ROUTE,
7
+ * "layout")` refreshes the index and every generated sitemap at once. A `layout`
8
+ * revalidation (not `page`) is required because the route has no `generateStaticParams`,
9
+ * so its slugs are generated on-demand and only a subtree invalidation reaches them.
10
+ * `[sitemap]` is Next's file-routing convention, which is why it lives in this integration
11
+ * and not in core.
12
+ */
13
+ const SITEMAP_ROUTE = `${SITEMAP_BASE}/[sitemap]`;
14
+ /**
15
+ * Next wrapper around the framework-agnostic {@link createCoreSitemapRoute}. It serves the
16
+ * same sitemap index and collection pages, and additionally asserts the route is mounted at
17
+ * {@link SITEMAP_ROUTE}: if the dynamic segment is not named `[sitemap]`, `params.sitemap`
18
+ * is missing and a wrong mount would silently break `revalidatePath(SITEMAP_ROUTE, "layout")`,
19
+ * so it throws loudly on the first request instead.
20
+ */
21
+ function createSitemapRoute$1(client, options) {
22
+ const handler = createSitemapRoute(client, options);
23
+ return async function GET(request, ctx) {
24
+ const params = ctx?.params ? await ctx.params : void 0;
25
+ if (params && params.sitemap === void 0) throw new Error(`createSitemapRoute must be mounted at "${SITEMAP_ROUTE}". The dynamic segment must be named [sitemap].`);
26
+ return handler(request);
27
+ };
28
+ }
29
+
30
+ //#endregion
31
+ export { SITEMAP_ROUTE, createSitemapRoute$1 as createSitemapRoute };
@@ -1,2 +1,4 @@
1
1
  import { KizloServerOptions, createKizlo } from "../integrations/nextjs/kizlo.server.js";
2
- export { KizloServerOptions, createKizlo };
2
+ import { createRobotsRoute } from "../integrations/nextjs/robots.js";
3
+ import { SITEMAP_ROUTE, createSitemapRoute } from "../integrations/nextjs/sitemap.js";
4
+ export { KizloServerOptions, SITEMAP_ROUTE, createKizlo, createRobotsRoute, createSitemapRoute };
@@ -1,3 +1,5 @@
1
+ import { createRobotsRoute } from "../integrations/nextjs/robots.js";
2
+ import { SITEMAP_ROUTE, createSitemapRoute } from "../integrations/nextjs/sitemap.js";
1
3
  import { createKizlo } from "../integrations/nextjs/kizlo.server.js";
2
4
 
3
- export { createKizlo };
5
+ export { SITEMAP_ROUTE, createKizlo, createRobotsRoute, createSitemapRoute };
package/dist/nextjs.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import { KizloClientOptions, createKizloClient } from "./integrations/nextjs/kizlo.client.js";
2
- export { KizloClientOptions, createKizloClient };
2
+ import { toMetadata } from "./integrations/nextjs/seo.js";
3
+ export { KizloClientOptions, createKizloClient, toMetadata };
package/dist/nextjs.js CHANGED
@@ -1,3 +1,4 @@
1
1
  import { createKizloClient } from "./integrations/nextjs/kizlo.client.js";
2
+ import { toMetadata } from "./integrations/nextjs/seo.js";
2
3
 
3
- export { createKizloClient };
4
+ export { createKizloClient, toMetadata };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.2.1";
2
+ var version = "0.3.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -1,5 +1,5 @@
1
- import { createProcedure } from "../shared/procedure.js";
2
1
  import { compare } from "../shared/crypto.js";
2
+ import { createProcedure } from "../shared/procedure.js";
3
3
  import { deserializeListMetadata } from "../shared/serialize.js";
4
4
  import { getPostTypeService } from "../post-type/service.js";
5
5
  import { parseIdentifier } from "../shared/identifier.js";
@@ -59,7 +59,7 @@ const POST_ROUTER_MAP = {
59
59
  }
60
60
  const data = response.data;
61
61
  if (input.query?.password && data.password) {
62
- if (!compare(input.query.password, data.password)) throw errors.POST_PASSWORD_INVALID();
62
+ if (!await compare(input.query.password, data.password)) throw errors.POST_PASSWORD_INVALID();
63
63
  return deserializePost({
64
64
  ...data,
65
65
  password: ""
package/dist/router.d.ts CHANGED
@@ -21,7 +21,7 @@ declare const ROUTER_MAP: {
21
21
  excerpt: string | null;
22
22
  commentsEnabled: boolean;
23
23
  protected: boolean;
24
- format: "image" | "link" | "standard" | "aside" | "chat" | "gallery" | "quote" | "status" | "video" | "audio";
24
+ format: "standard" | "aside" | "chat" | "gallery" | "link" | "image" | "quote" | "status" | "video" | "audio";
25
25
  sticky: boolean;
26
26
  author: {
27
27
  id: number;
@@ -150,7 +150,7 @@ declare const ROUTER_MAP: {
150
150
  include?: string | number | (string | number)[] | undefined;
151
151
  offset?: string | number | undefined;
152
152
  order?: "asc" | "desc" | undefined;
153
- orderby?: "title" | "author" | "date" | "id" | "parent" | "slug" | "include" | "relevance" | "include_slugs" | undefined;
153
+ orderby?: "date" | "id" | "slug" | "title" | "author" | "include" | "parent" | "relevance" | "include_slugs" | undefined;
154
154
  searchColumns?: string[] | undefined;
155
155
  slug?: string | string[] | undefined;
156
156
  taxRelation?: "AND" | "OR" | undefined;
@@ -169,7 +169,7 @@ declare const ROUTER_MAP: {
169
169
  excerpt: string | null;
170
170
  commentsEnabled: boolean;
171
171
  protected: boolean;
172
- format: "image" | "link" | "standard" | "aside" | "chat" | "gallery" | "quote" | "status" | "video" | "audio";
172
+ format: "standard" | "aside" | "chat" | "gallery" | "link" | "image" | "quote" | "status" | "video" | "audio";
173
173
  sticky: boolean;
174
174
  author: {
175
175
  id: number;
@@ -299,12 +299,70 @@ declare const ROUTER_MAP: {
299
299
  }>;
300
300
  };
301
301
  seo: {
302
- sitemaps: Procedure<"internal", {}, {
303
- key: string;
304
- type: "author" | "post_type" | "taxonomy";
305
- pages: number;
306
- lastmod: string;
307
- }[], DefinedErrorMapLike>;
302
+ homepage: Procedure<"internal", {}, {
303
+ head: {
304
+ title: string;
305
+ canonical: string;
306
+ robots: {
307
+ index: string;
308
+ follow: string;
309
+ maxSnippet: string;
310
+ maxImagePreview: string;
311
+ maxVideoPreview: string;
312
+ };
313
+ og: {
314
+ locale: string;
315
+ type: string;
316
+ title: string;
317
+ url: string;
318
+ siteName: string;
319
+ description: string;
320
+ image: {
321
+ url: string;
322
+ width: string;
323
+ height: string;
324
+ type: string;
325
+ alt: string | null;
326
+ } | null;
327
+ };
328
+ twitter: {
329
+ card: "summary" | "summary_large_image";
330
+ title: string;
331
+ site: string | null;
332
+ creator: string | null;
333
+ description: string;
334
+ image: string | null;
335
+ };
336
+ article: {
337
+ publishedTime: string;
338
+ modifiedTime: string;
339
+ author: string;
340
+ authorUrl: string;
341
+ section: string;
342
+ } | null;
343
+ };
344
+ schema: {
345
+ "@context": "https://schema.org";
346
+ "@graph": Record<string, unknown>[];
347
+ };
348
+ }, DefinedErrorMapLike>;
349
+ sitemaps: {
350
+ list: Procedure<"internal", {}, {
351
+ key: string;
352
+ type: "author" | "post_type" | "taxonomy";
353
+ pages: number;
354
+ lastmod: string;
355
+ }[], DefinedErrorMapLike>;
356
+ index: Procedure<"internal", {}, {
357
+ origin: string;
358
+ sitemaps: {
359
+ key: string;
360
+ type: "author" | "post_type" | "taxonomy";
361
+ pages: number;
362
+ lastmod: string;
363
+ }[];
364
+ }, DefinedErrorMapLike>;
365
+ };
308
366
  robots: Procedure<"internal", {}, {
309
367
  sitemaps: string[];
310
368
  rules: {
@@ -342,7 +400,7 @@ declare const ROUTER_MAP: {
342
400
  include?: string | number | (string | number)[] | undefined;
343
401
  offset?: string | number | undefined;
344
402
  order?: "asc" | "desc" | undefined;
345
- orderby?: "title" | "author" | "date" | "id" | "parent" | "slug" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
403
+ orderby?: "date" | "id" | "slug" | "title" | "author" | "include" | "parent" | "relevance" | "include_slugs" | "menu_order" | undefined;
346
404
  searchColumns?: string[] | undefined;
347
405
  slug?: string | string[] | undefined;
348
406
  taxRelation?: "AND" | "OR" | undefined;
@@ -355,7 +413,7 @@ declare const ROUTER_MAP: {
355
413
  id: number;
356
414
  href: string;
357
415
  name: string;
358
- type: "custom" | "page" | "post" | "category" | "product" | "product_cat" | "product_tag";
416
+ type: "custom" | "post" | "page" | "category" | "product" | "product_cat" | "product_tag";
359
417
  description: string;
360
418
  meta: {
361
419
  [x: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/any | {
@@ -413,7 +471,7 @@ declare const ROUTER_MAP: {
413
471
  include?: string | number | (string | number)[] | undefined;
414
472
  offset?: string | number | undefined;
415
473
  order?: "asc" | "desc" | undefined;
416
- orderby?: "title" | "author" | "date" | "id" | "parent" | "slug" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
474
+ orderby?: "date" | "id" | "slug" | "title" | "author" | "include" | "parent" | "relevance" | "include_slugs" | "menu_order" | undefined;
417
475
  searchColumns?: string[] | undefined;
418
476
  slug?: string | string[] | undefined;
419
477
  taxRelation?: "AND" | "OR" | undefined;
@@ -503,7 +561,7 @@ declare const ROUTER_MAP: {
503
561
  include?: string | number | (string | number)[] | undefined;
504
562
  offset?: string | number | undefined;
505
563
  order?: "asc" | "desc" | undefined;
506
- orderby?: "date" | "id" | "parent" | "post" | "include" | undefined;
564
+ orderby?: "date" | "id" | "post" | "include" | "parent" | undefined;
507
565
  parent?: string | number | (string | number)[] | undefined;
508
566
  parentExclude?: string | number | (string | number)[] | undefined;
509
567
  post?: string | number | (string | number)[] | undefined;
package/dist/seo/index.js CHANGED
@@ -1,20 +1,85 @@
1
1
  import { createProcedure } from "../shared/procedure.js";
2
2
  import { WP_KIZLO_BASE } from "../wordpress/constants.js";
3
- import { ListSitemapUrlInput, Robots, SitemapList, SitemapUrlList } from "./schema.js";
3
+ import { ListSitemapUrlInput, Robots, Seo, SitemapIndex, SitemapList, SitemapUrlList } from "./schema.js";
4
4
 
5
5
  //#region src/seo/index.ts
6
6
  const SEO_ROUTER_MAP = {
7
- sitemaps: createProcedure({
7
+ homepage: createProcedure({
8
8
  scope: "internal",
9
- output: SitemapList
9
+ output: Seo
10
10
  }, async ({ context, errors }) => {
11
- const response = await context.service.wordpress.get("/seo/sitemaps", { base: WP_KIZLO_BASE });
11
+ const response = await context.service.wordpress.get("/seo/homepage", { base: WP_KIZLO_BASE });
12
12
  if (response.error) {
13
- context.logger.error("List sitemaps unhandled error", response.error);
13
+ context.logger.error("Get homepage seo unhandled error", response.error);
14
14
  throw errors.INTERNAL_SERVER_ERROR();
15
15
  }
16
- return response.data;
16
+ const { head } = response.data;
17
+ return {
18
+ head: {
19
+ title: head.title,
20
+ canonical: head.canonical,
21
+ robots: {
22
+ index: head.robots.index,
23
+ follow: head.robots.follow,
24
+ maxSnippet: head.robots["max-snippet"],
25
+ maxImagePreview: head.robots["max-image-preview"],
26
+ maxVideoPreview: head.robots["max-video-preview"]
27
+ },
28
+ og: {
29
+ locale: head.og.locale,
30
+ type: head.og.type,
31
+ title: head.og.title,
32
+ url: head.og.url,
33
+ siteName: head.og.site_name,
34
+ description: head.og.description ?? "",
35
+ image: head.og.image
36
+ },
37
+ twitter: {
38
+ card: head.twitter.card,
39
+ title: head.twitter.title,
40
+ site: head.twitter.site,
41
+ creator: head.twitter.creator,
42
+ description: head.twitter.description ?? "",
43
+ image: head.twitter.image
44
+ },
45
+ article: head.article && {
46
+ publishedTime: head.article.published_time,
47
+ modifiedTime: head.article.modified_time,
48
+ author: head.article.author,
49
+ authorUrl: head.article.author_url,
50
+ section: head.article.section
51
+ }
52
+ },
53
+ schema: response.data.schema
54
+ };
17
55
  }),
56
+ sitemaps: {
57
+ list: createProcedure({
58
+ scope: "internal",
59
+ output: SitemapList
60
+ }, async ({ context, errors }) => {
61
+ const response = await context.service.wordpress.get("/seo/sitemaps", { base: WP_KIZLO_BASE });
62
+ if (response.error) {
63
+ context.logger.error("List sitemaps unhandled error", response.error);
64
+ throw errors.INTERNAL_SERVER_ERROR();
65
+ }
66
+ return response.data;
67
+ }),
68
+ index: createProcedure({
69
+ scope: "internal",
70
+ output: SitemapIndex
71
+ }, async ({ context, errors }) => {
72
+ const response = await context.service.wordpress.get("/seo/sitemaps/index", { base: WP_KIZLO_BASE });
73
+ if (response.error) {
74
+ context.logger.error("Get sitemap index unhandled error", response.error);
75
+ throw errors.INTERNAL_SERVER_ERROR();
76
+ }
77
+ return {
78
+ origin: response.data.origin,
79
+ sitemaps: response.data.sitemaps
80
+ };
81
+ })
82
+ },
18
83
  robots: createProcedure({
19
84
  scope: "internal",
20
85
  output: Robots
@@ -0,0 +1,18 @@
1
+ //#region src/seo/robots.ts
2
+ function createRobotsRoute(client) {
3
+ return async function GET(_request) {
4
+ const data = await client.seo.robots.call();
5
+ const groups = data.rules.map((rule) => {
6
+ const lines = [`User-agent: ${rule.userAgent}`];
7
+ for (const path of rule.allow) lines.push(`Allow: ${path}`);
8
+ for (const path of rule.disallow) lines.push(`Disallow: ${path}`);
9
+ return lines.join("\n");
10
+ });
11
+ const sitemaps = data.sitemaps.map((url) => `Sitemap: ${url}`).join("\n");
12
+ const blocks = [groups.join("\n\n"), sitemaps].filter(Boolean);
13
+ return new Response(`${blocks.join("\n\n")}\n`, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
14
+ };
15
+ }
16
+
17
+ //#endregion
18
+ export { createRobotsRoute };
@@ -131,6 +131,20 @@ declare const SitemapList: z.ZodArray<z.ZodObject<{
131
131
  lastmod: z.ZodString;
132
132
  }, z.core.$strip>>;
133
133
  type SitemapList = z.infer<typeof SitemapList>;
134
+ declare const SitemapIndex: z.ZodObject<{
135
+ origin: z.ZodString;
136
+ sitemaps: z.ZodArray<z.ZodObject<{
137
+ key: z.ZodString;
138
+ type: z.ZodEnum<{
139
+ author: "author";
140
+ post_type: "post_type";
141
+ taxonomy: "taxonomy";
142
+ }>;
143
+ pages: z.ZodNumber;
144
+ lastmod: z.ZodString;
145
+ }, z.core.$strip>>;
146
+ }, z.core.$strip>;
147
+ type SitemapIndex = z.infer<typeof SitemapIndex>;
134
148
  declare const SitemapUrlImage: z.ZodObject<{
135
149
  loc: z.ZodString;
136
150
  title: z.ZodString;
@@ -193,4 +207,4 @@ declare const Robots: z.ZodObject<{
193
207
  }, z.core.$strip>;
194
208
  type Robots = z.infer<typeof Robots>;
195
209
  //#endregion
196
- export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
210
+ export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
@@ -64,6 +64,10 @@ const Sitemap = z.object({
64
64
  lastmod: z.string()
65
65
  });
66
66
  const SitemapList = z.array(Sitemap);
67
+ const SitemapIndex = z.object({
68
+ origin: z.string(),
69
+ sitemaps: SitemapList
70
+ });
67
71
  const SitemapUrlImage = z.object({
68
72
  loc: z.string(),
69
73
  title: z.string()
@@ -93,4 +97,4 @@ const Robots = z.object({
93
97
  });
94
98
 
95
99
  //#endregion
96
- export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
100
+ export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
@@ -0,0 +1,39 @@
1
+ import { SitemapUrl } from "./schema.js";
2
+ import { SitemapIndexEntry } from "./utils.js";
3
+ import { S2SClient } from "../kizlo.js";
4
+
5
+ //#region src/seo/sitemap.d.ts
6
+
7
+ /**
8
+ * Every generated sitemap is served from `/sitemaps`: the index at `/sitemaps/index.xml`
9
+ * and each collection page at `/sitemaps/{key}.xml` (later pages `-{n}`). The base is fixed
10
+ * because the index `<loc>` entries (built by `sitemapEntryPath`) point at it, so the route
11
+ * must be mounted there for the links to resolve. How the route is mounted is a
12
+ * framework concern; this handler only cares about the trailing slug.
13
+ */
14
+ declare const SITEMAP_BASE = "/sitemaps";
15
+ /** Slug that serves the sitemap index (`/sitemaps/index.xml`) rather than a collection. */
16
+ declare const SITEMAP_INDEX_SLUG = "index.xml";
17
+ /**
18
+ * A sitemap not backed by WordPress (e.g. docs pages living in the app). It joins the
19
+ * same index/urlset pipeline: `entry` adds it to the index, `urls` serves its pages.
20
+ */
21
+ interface SitemapRouteExtra {
22
+ entry: SitemapIndexEntry;
23
+ urls: (origin: string, page: number) => SitemapUrl[] | Promise<SitemapUrl[]>;
24
+ }
25
+ interface CreateSitemapRouteOptions {
26
+ /** Extra, non-WordPress sitemaps to fold into the index and serve. */
27
+ extra?: SitemapRouteExtra[];
28
+ }
29
+ /**
30
+ * Builds a Web `Request` -> `Response` handler that serves the sitemap index
31
+ * (`/sitemaps/index.xml`) and every collection page, merging any `extra` sitemaps in.
32
+ * Framework-agnostic: the slug is the last path segment of the request URL, so this runs
33
+ * as-is on any web-standard server (Hono, a plain fetch handler, a Next route handler).
34
+ * Mounting it at the right path is the caller's job; framework wrappers (e.g. the Next
35
+ * integration) add mount assertions on top.
36
+ */
37
+ declare function createSitemapRoute(client: S2SClient<[]>, options?: CreateSitemapRouteOptions): (request: Request) => Promise<Response>;
38
+ //#endregion
39
+ export { CreateSitemapRouteOptions, SITEMAP_BASE, SITEMAP_INDEX_SLUG, SitemapRouteExtra, createSitemapRoute };