@tdacorp/hyle-sdk 0.1.1 → 0.2.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.
package/dist/next.cjs CHANGED
@@ -21,13 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var next_exports = {};
22
22
  __export(next_exports, {
23
23
  createRevalidateRoute: () => createRevalidateRoute,
24
+ fetchResolver: () => fetchResolver,
24
25
  hyleMetadata: () => hyleMetadata,
25
- hylePage: () => hylePage
26
+ hylePage: () => hylePage,
27
+ hyleSitemap: () => hyleSitemap,
28
+ workspaceTags: () => workspaceTags
26
29
  });
27
30
  module.exports = __toCommonJS(next_exports);
28
31
 
29
32
  // src/next/page.tsx
30
- var import_navigation = require("next/navigation");
33
+ var import_navigation2 = require("next/navigation");
31
34
 
32
35
  // src/errors.ts
33
36
  var Cp1ClientError = class extends Error {
@@ -268,6 +271,46 @@ function okResponse(purged) {
268
271
  });
269
272
  }
270
273
 
274
+ // src/sitemap.ts
275
+ var PAGE_LIMIT = 100;
276
+ var DEFAULT_MAX_PAGES = 20;
277
+ var SECTION_SLUG = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
278
+ var UNSAFE_PATH = /[\\?#<>&"'\u0000- \u007f]/;
279
+ async function listPublicationPaths(options) {
280
+ const maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;
281
+ const prefix = `${options.basePath}/`;
282
+ const indexes = [options.basePath || "/"];
283
+ for (const slug of options.sections ?? []) if (slug !== "posts" && SECTION_SLUG.test(slug)) indexes.push(`${options.basePath}/${slug}`);
284
+ const found = /* @__PURE__ */ new Map();
285
+ let complete = true;
286
+ try {
287
+ for (const index of new Set(indexes)) {
288
+ let cursor;
289
+ for (let page = 0; page < maxPages; page++) {
290
+ const result = await options.client.resolve({
291
+ path: index,
292
+ limit: PAGE_LIMIT,
293
+ ...options.locale !== void 0 ? { locale: options.locale } : {},
294
+ ...cursor !== void 0 ? { cursor } : {}
295
+ });
296
+ if (result.kind !== "list") {
297
+ complete = false;
298
+ break;
299
+ }
300
+ for (const item of result.items) {
301
+ if (item.canonical_path.startsWith(prefix) && !UNSAFE_PATH.test(item.canonical_path)) found.set(item.canonical_path, item.published_at);
302
+ }
303
+ if (!result.next_cursor) break;
304
+ if (page === maxPages - 1) complete = false;
305
+ cursor = result.next_cursor;
306
+ }
307
+ }
308
+ } catch {
309
+ complete = false;
310
+ }
311
+ return { paths: [...found].map(([path, publishedAt]) => ({ path, publishedAt })), complete };
312
+ }
313
+
271
314
  // src/react/content.tsx
272
315
  var import_hast_util_to_jsx_runtime = require("hast-util-to-jsx-runtime");
273
316
  var import_react = require("react");
@@ -513,7 +556,10 @@ var en = {
513
556
  rdBody: "You are being taken to its new address.",
514
557
  from: "From",
515
558
  to: "To",
516
- rdFallback: "If nothing happens, go to the new address."
559
+ rdFallback: "If nothing happens, go to the new address.",
560
+ unKind: "Unavailable",
561
+ unTitle: "This page is temporarily unavailable",
562
+ unBody: "The content could not be loaded. Please try again in a moment."
517
563
  };
518
564
  var fr = {
519
565
  toc: "Sur cette page",
@@ -549,7 +595,10 @@ var fr = {
549
595
  rdBody: "Vous \xEAtes redirig\xE9 vers sa nouvelle adresse.",
550
596
  from: "De",
551
597
  to: "Vers",
552
- rdFallback: "Si rien ne se passe, allez \xE0 la nouvelle adresse."
598
+ rdFallback: "Si rien ne se passe, allez \xE0 la nouvelle adresse.",
599
+ unKind: "Indisponible",
600
+ unTitle: "Cette page est momentan\xE9ment indisponible",
601
+ unBody: "Le contenu n'a pas pu \xEAtre charg\xE9. R\xE9essayez dans un instant."
553
602
  };
554
603
  var es = {
555
604
  toc: "En esta p\xE1gina",
@@ -585,7 +634,10 @@ var es = {
585
634
  rdBody: "Te estamos llevando a su nueva direcci\xF3n.",
586
635
  from: "Desde",
587
636
  to: "A",
588
- rdFallback: "Si no pasa nada, ve a la nueva direcci\xF3n."
637
+ rdFallback: "Si no pasa nada, ve a la nueva direcci\xF3n.",
638
+ unKind: "No disponible",
639
+ unTitle: "Esta p\xE1gina no est\xE1 disponible por el momento",
640
+ unBody: "No se pudo cargar el contenido. Int\xE9ntalo de nuevo en un momento."
589
641
  };
590
642
  var TABLE = { en, fr, es };
591
643
  function stringsFor(locale) {
@@ -932,6 +984,9 @@ function DocsPage({ article, ...base }) {
932
984
  ] }) });
933
985
  }
934
986
 
987
+ // src/next/options.ts
988
+ var import_navigation = require("next/navigation");
989
+
935
990
  // src/next/paths.ts
936
991
  function normalizeBasePath(basePath) {
937
992
  const value = (basePath ?? "").replace(/\/+$/, "");
@@ -939,6 +994,7 @@ function normalizeBasePath(basePath) {
939
994
  if (!/^\/[^\s?#\\]+$/.test(value) || value.includes("//")) throw new TypeError(`basePath must look like "/learn", got "${basePath}"`);
940
995
  return value;
941
996
  }
997
+ var MAX_PATH_LENGTH = 1024;
942
998
  function sitePathFrom(basePath, segments) {
943
999
  const decoded = [];
944
1000
  for (const raw of segments ?? []) {
@@ -948,10 +1004,12 @@ function sitePathFrom(basePath, segments) {
948
1004
  } catch {
949
1005
  return void 0;
950
1006
  }
1007
+ if (segment !== segment.normalize("NFC")) return void 0;
951
1008
  if (segment === "" || segment === "." || segment === ".." || /[/\\\u0000-\u001f\u007f]/.test(segment)) return void 0;
952
1009
  decoded.push(segment);
953
1010
  }
954
- return decoded.length === 0 ? basePath || "/" : `${basePath}/${decoded.join("/")}`;
1011
+ const path = decoded.length === 0 ? basePath || "/" : `${basePath}/${decoded.join("/")}`;
1012
+ return path.length > MAX_PATH_LENGTH ? void 0 : path;
955
1013
  }
956
1014
  function safeRedirectTarget(to) {
957
1015
  if (typeof to !== "string" || !to.startsWith("/") || to.startsWith("//")) return void 0;
@@ -961,19 +1019,24 @@ function safeRedirectTarget(to) {
961
1019
  // src/next/resolve.ts
962
1020
  var DEFAULT_REVALIDATE = 300;
963
1021
  var MAX_TAGS2 = 64;
964
- function fetchTags(workspaceId, sections = []) {
1022
+ function workspaceTags(workspaceId, sections = []) {
1023
+ const workspaceTag = `hyle:1:ws:${workspaceId}`;
1024
+ if (!isValidCacheTag(workspaceTag)) throw new TypeError("hyle: `workspaceId` is not a valid workspace id (a uuid)");
965
1025
  const slugs = /* @__PURE__ */ new Set(["posts", ...sections]);
966
- return [`hyle:1:ws:${workspaceId}`, ...[...slugs].map((s) => `hyle:1:sec:${workspaceId}:${s}`)].filter(isValidCacheTag).slice(0, MAX_TAGS2);
1026
+ const tags = [...slugs].map((slug) => `hyle:1:sec:${workspaceId}:${slug}`);
1027
+ if (!tags.every(isValidCacheTag)) throw new TypeError("hyle: a configured `sections` slug cannot be used in a cache tag (printable ASCII, at most 200 characters)");
1028
+ return [workspaceTag, ...tags].slice(0, MAX_TAGS2);
967
1029
  }
968
1030
  function fetchResolver(options) {
969
1031
  if (!options.workspaceId) throw new TypeError("hylePage: `workspaceId` is required for the fetch cache (or pass a cachedResolver)");
970
1032
  const client = createHyleClient({ baseUrl: options.baseUrl, token: options.token });
971
1033
  const revalidate = options.revalidate ?? DEFAULT_REVALIDATE;
972
- const tags = fetchTags(options.workspaceId, options.sections);
973
- return (path, locale, limit) => client.resolve({
1034
+ const tags = workspaceTags(options.workspaceId, options.sections);
1035
+ return (path, locale, limit, cursor) => client.resolve({
974
1036
  path,
975
1037
  ...locale !== void 0 ? { locale } : {},
976
1038
  ...limit !== void 0 ? { limit } : {},
1039
+ ...cursor !== void 0 ? { cursor } : {},
977
1040
  init: { next: { revalidate, tags } }
978
1041
  });
979
1042
  }
@@ -982,77 +1045,135 @@ function fetchResolver(options) {
982
1045
  function resolverFor(options) {
983
1046
  return options.resolver ?? fetchResolver(options);
984
1047
  }
985
- var LOCALE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/;
1048
+ var CURSOR = /^[A-Za-z0-9_-]{1,256}$/;
1049
+ var validCursor = (value) => typeof value === "string" && CURSOR.test(value) ? value : void 0;
1050
+ var LOCALE_TAG = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
1051
+ var MAX_LOCALE_LENGTH = 35;
986
1052
  function routeContext(options, params) {
987
1053
  const basePath = normalizeBasePath(options.basePath);
988
1054
  const segments = params[options.param ?? "hyle"];
989
1055
  const path = sitePathFrom(basePath, typeof segments === "string" ? [segments] : segments);
990
1056
  const given = typeof options.locale === "function" ? options.locale(params) : options.locale;
991
- const locale = typeof given === "string" && LOCALE_TAG.test(given) ? given : void 0;
1057
+ const locale = typeof given === "string" && given.length <= MAX_LOCALE_LENGTH && LOCALE_TAG.test(given) ? given : void 0;
992
1058
  const prefix = options.localePrefix ?? (() => "");
993
1059
  const toHref = (sitePath, linkLocale = locale) => (linkLocale ? prefix(linkLocale) : "") + sitePath;
994
1060
  return { basePath, path, locale, toHref };
995
1061
  }
1062
+ function isRejectedRequest(error) {
1063
+ (0, import_navigation.unstable_rethrow)(error);
1064
+ return error instanceof Cp1ClientError && error.status >= 400 && error.status < 500 && ![401, 403, 429].includes(error.status);
1065
+ }
996
1066
 
997
1067
  // src/next/page.tsx
998
1068
  var import_jsx_runtime5 = require("react/jsx-runtime");
999
1069
  function hylePage(options) {
1000
1070
  const resolve = resolverFor(options);
1001
- return async function HylePage({ params }) {
1071
+ return async function HylePage({ params, searchParams }) {
1002
1072
  const { path, locale, toHref } = routeContext(options, await params ?? {});
1003
- if (path === void 0) (0, import_navigation.notFound)();
1004
- const result = await resolve(path, locale, options.listLimit);
1005
- switch (result.kind) {
1006
- case "article": {
1007
- const props = { article: result, components: options.components, slots: options.slots, locale, toHref };
1008
- return options.layout === "docs" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DocsPage, { ...props }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArticlePage, { ...props });
1009
- }
1010
- case "list":
1011
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ListPage, { list: result, components: options.components, slots: options.slots, locale, toHref });
1012
- case "redirect": {
1013
- const to = safeRedirectTarget(result.to);
1014
- if (to === void 0) throw new Cp1ClientError("INVALID_RESPONSE", "hyle returned an unsafe redirect target", 502, "");
1015
- const target = safeRedirectTarget(toHref(to));
1016
- if (target === void 0) throw new Cp1ClientError("INVALID_RESPONSE", "hyle returned an unsafe redirect target", 502, "");
1017
- if (target === toHref(path)) (0, import_navigation.notFound)();
1018
- return result.status === 301 ? (0, import_navigation.permanentRedirect)(target) : (0, import_navigation.redirect)(target);
1073
+ if (path === void 0) (0, import_navigation2.notFound)();
1074
+ try {
1075
+ const cursor = options.paginate === "searchParams" ? validCursor((await searchParams)?.cursor) : void 0;
1076
+ const result = await resolve(path, locale, options.listLimit, cursor);
1077
+ switch (result.kind) {
1078
+ case "article": {
1079
+ const props = { article: result, components: options.components, slots: options.slots, locale, toHref };
1080
+ return options.layout === "docs" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DocsPage, { ...props }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArticlePage, { ...props });
1081
+ }
1082
+ case "list": {
1083
+ const list = options.list?.title ? { ...result, section: { ...result.section, title: options.list.title } } : result;
1084
+ const next = options.paginate === "searchParams" ? validCursor(result.next_cursor) : void 0;
1085
+ const olderHref = next ? `${toHref(path)}?cursor=${next}` : void 0;
1086
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ListPage, { list, ...olderHref ? { olderHref } : {}, ...options.list?.description ? { description: options.list.description } : {}, components: options.components, slots: options.slots, locale, toHref });
1087
+ }
1088
+ case "redirect": {
1089
+ const to = safeRedirectTarget(result.to);
1090
+ if (to === void 0) throw new Cp1ClientError("INVALID_RESPONSE", "hyle returned an unsafe redirect target", 502, "");
1091
+ const target = safeRedirectTarget(toHref(to));
1092
+ if (target === void 0) throw new Cp1ClientError("INVALID_RESPONSE", "hyle returned an unsafe redirect target", 502, "");
1093
+ if (target === toHref(path)) (0, import_navigation2.notFound)();
1094
+ return result.status === 301 ? (0, import_navigation2.permanentRedirect)(target) : (0, import_navigation2.redirect)(target);
1095
+ }
1096
+ case "notfound":
1097
+ case "disabled":
1098
+ (0, import_navigation2.notFound)();
1019
1099
  }
1020
- case "notfound":
1021
- case "disabled":
1022
- (0, import_navigation.notFound)();
1100
+ } catch (error) {
1101
+ if (isRejectedRequest(error)) (0, import_navigation2.notFound)();
1102
+ if (!options.onError) throw error;
1103
+ console.error(`hyle: ${error instanceof Error ? error.name : "UnknownError"}`);
1104
+ return options.onError(error);
1023
1105
  }
1024
1106
  };
1025
1107
  }
1026
1108
 
1109
+ // src/next/sitemap.ts
1110
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}T/;
1111
+ function httpsOrigin(origin) {
1112
+ let url;
1113
+ try {
1114
+ url = new URL(origin);
1115
+ } catch {
1116
+ throw new TypeError("hyleSitemap: `origin` must be an https origin such as https://example.com");
1117
+ }
1118
+ if (url.protocol !== "https:" || url.origin !== origin.replace(/\/$/, "")) {
1119
+ throw new TypeError("hyleSitemap: `origin` must be an https origin with no path, query or credentials");
1120
+ }
1121
+ return url.origin;
1122
+ }
1123
+ async function hyleSitemap(options) {
1124
+ const origin = httpsOrigin(options.origin);
1125
+ const resolve = resolverFor(options);
1126
+ const locale = typeof options.locale === "string" ? options.locale : void 0;
1127
+ const { basePath, toHref } = routeContext({ ...options, locale: () => locale }, {});
1128
+ const client = { resolve: ({ path, locale: l, limit, cursor }) => resolve(path, l, limit, cursor) };
1129
+ const { paths, complete } = await listPublicationPaths({ client, basePath, ...options.sections ? { sections: options.sections } : {}, ...locale ? { locale } : {} });
1130
+ if (!complete && (options.onIncomplete === "throw" || options.onIncomplete === void 0 && paths.length === 0)) {
1131
+ throw new Error("hyleSitemap: hyle could not be listed completely (see onIncomplete)");
1132
+ }
1133
+ return paths.map(({ path, publishedAt }) => ({
1134
+ url: origin + encodeURI(toHref(path)),
1135
+ ...ISO_DATE.test(publishedAt) && !Number.isNaN(Date.parse(publishedAt)) ? { lastModified: publishedAt } : {}
1136
+ }));
1137
+ }
1138
+
1027
1139
  // src/next/metadata.ts
1028
1140
  function hyleMetadata(options) {
1029
1141
  const resolve = resolverFor(options);
1030
1142
  return async function generateMetadata({ params }) {
1031
1143
  const { path, locale, toHref } = routeContext(options, await params ?? {});
1032
1144
  if (path === void 0) return {};
1033
- const result = await resolve(path, locale, options.listLimit);
1034
- if (result.kind === "list") return { title: result.section.title };
1035
- if (result.kind !== "article") return {};
1036
- const { publication: pub, alternates } = result;
1037
- const canonical = toHref(pub.canonical_path, pub.locale);
1038
- const description = pub.description ?? void 0;
1039
- return {
1040
- title: pub.title,
1041
- ...description ? { description } : {},
1042
- alternates: {
1043
- canonical,
1044
- ...alternates.length > 1 ? { languages: Object.fromEntries(alternates.map((a) => [a.locale, toHref(a.canonical_path, a.locale)])) } : {}
1045
- },
1046
- openGraph: {
1047
- type: "article",
1145
+ try {
1146
+ const result = await resolve(path, locale, options.listLimit);
1147
+ if (result.kind === "list") {
1148
+ const title = options.list?.title || result.section.title;
1149
+ const description2 = options.list?.description;
1150
+ return { title, ...description2 ? { description: description2 } : {}, openGraph: { title, ...description2 ? { description: description2 } : {} } };
1151
+ }
1152
+ if (result.kind !== "article") return {};
1153
+ const { publication: pub, alternates } = result;
1154
+ const canonical = toHref(pub.canonical_path, pub.locale);
1155
+ const description = pub.description ?? void 0;
1156
+ return {
1048
1157
  title: pub.title,
1049
1158
  ...description ? { description } : {},
1050
- url: canonical,
1051
- locale: pub.locale,
1052
- publishedTime: pub.published_at,
1053
- modifiedTime: pub.updated_at
1054
- }
1055
- };
1159
+ alternates: {
1160
+ canonical,
1161
+ ...alternates.length > 1 ? { languages: Object.fromEntries(alternates.map((a) => [a.locale, toHref(a.canonical_path, a.locale)])) } : {}
1162
+ },
1163
+ openGraph: {
1164
+ type: "article",
1165
+ title: pub.title,
1166
+ ...description ? { description } : {},
1167
+ url: canonical,
1168
+ locale: pub.locale,
1169
+ publishedTime: pub.published_at,
1170
+ modifiedTime: pub.updated_at
1171
+ }
1172
+ };
1173
+ } catch (error) {
1174
+ if (isRejectedRequest(error)) return {};
1175
+ throw error;
1176
+ }
1056
1177
  };
1057
1178
  }
1058
1179
 
@@ -1079,6 +1200,9 @@ function createRevalidateRoute(options) {
1079
1200
  // Annotate the CommonJS export names for ESM import in node:
1080
1201
  0 && (module.exports = {
1081
1202
  createRevalidateRoute,
1203
+ fetchResolver,
1082
1204
  hyleMetadata,
1083
- hylePage
1205
+ hylePage,
1206
+ hyleSitemap,
1207
+ workspaceTags
1084
1208
  });
package/dist/next.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { H as HyleComponents, a as HyleSlots } from './types-DLWvqanV.cjs';
3
- import { F as FetchResolverOptions, H as HyleResolver } from './resolve-DrmTGaCr.cjs';
4
- import { Metadata } from 'next';
3
+ import { F as FetchResolverOptions, H as HyleResolver } from './resolve-B_p0p4Zn.cjs';
4
+ export { f as fetchResolver, w as workspaceTags } from './resolve-B_p0p4Zn.cjs';
5
+ import { MetadataRoute, Metadata } from 'next';
5
6
  import { a as RevalidationCallbackRoute } from './revalidation-callback-D7Ms_Puz.cjs';
6
7
  import './types-DjXf6iPo.cjs';
7
8
 
@@ -28,6 +29,33 @@ interface CommonOptions {
28
29
  readonly layout?: "article" | "docs";
29
30
  /** Items per list page (server default 20, maximum 100). */
30
31
  readonly listLimit?: number;
32
+ /**
33
+ * Heading and description of the list page (the mount path's index). hyle names the default section after its slug
34
+ * ("Posts"); `list.title` replaces that in the page's h1, the `<title>` and Open Graph, and `list.description` fills the
35
+ * page description, `<meta name="description">` and Open Graph. Each falls back separately: title to the hyle section
36
+ * title, description to none. Article pages are unaffected.
37
+ */
38
+ readonly list?: {
39
+ readonly title?: string;
40
+ readonly description?: string;
41
+ };
42
+ /**
43
+ * `"searchParams"` makes list pages paginate: `?cursor=` is read, sent to hyle, and an "Older posts" link is built from
44
+ * `next_cursor`. Default off, and the trade-off is real: reading `searchParams` makes the route dynamic, so it can no longer
45
+ * be statically generated or served from ISR (hyle's fetch cache still applies). Leave it off when one page of `listLimit`
46
+ * items is enough. A malformed `cursor` is ignored (the first page is shown); one that is well-formed but unknown to hyle is a 404.
47
+ */
48
+ readonly paginate?: "searchParams";
49
+ /**
50
+ * Opt-in custom UI for transient or operational hyle failures (network error, timeout, 5xx, 401, 403, 429, a malformed
51
+ * response, a refused redirect target). Without it `hylePage` rethrows them, so Next keeps serving the last good ISR page
52
+ * when a revalidation fails (and a failure on a first render is an uncached 500). With it, the returned element is a NORMAL
53
+ * render: it is cached for the revalidate window and replaces the last good page for that long, so prefer a boundary
54
+ * (`error.tsx`) unless you want that. The `error` can carry `Cp1ClientError.message` and `requestId`: never render it.
55
+ * Only its name is logged. A 4xx rejection of the request (400, 404, ...) is always `notFound()` and never reaches this, and
56
+ * `notFound()` and redirects are Next control flow that always propagate.
57
+ */
58
+ readonly onError?: (error: unknown) => ReactElement;
31
59
  }
32
60
  /**
33
61
  * Pass either the fetch-cache settings (works with no host configuration) or a `resolver` from
@@ -40,6 +68,8 @@ type HyleNextOptions = CommonOptions & (({
40
68
  });
41
69
  interface PageProps {
42
70
  readonly params?: Promise<RouteParams>;
71
+ /** Read only with `paginate: "searchParams"`. */
72
+ readonly searchParams?: Promise<Record<string, string | string[] | undefined>>;
43
73
  }
44
74
 
45
75
  /**
@@ -50,17 +80,45 @@ interface PageProps {
50
80
  * in front, and 301 maps to permanentRedirect (308) while 307 maps to redirect (temporary). A target equal to the current
51
81
  * URL would loop, so it is treated as not found.
52
82
  *
83
+ * Failure: a 4xx rejection of the request (400, 404, ...) is `notFound()`. Every other failure (network error, timeout, 5xx,
84
+ * 401, 403, 429, a malformed response, a refused redirect target) is RETHROWN, so under ISR Next keeps serving the last good
85
+ * page when a revalidation fails, and a first-render failure is an uncached 500 rather than a cached soft 200. `onError`
86
+ * opts in to a custom element instead (see its docs). `notFound()` and redirects always propagate (`unstable_rethrow`).
87
+ *
53
88
  * Host requirement: no parent layout or page above the mount may export `dynamicParams = false`. Next inherits it, the
54
89
  * route is compiled with `"fallback": false` and every article not known at build time is a hard 404 (it falls through
55
90
  * to a `[locale]/[...rest]` catch-all). A page-level `dynamicParams = true` does not override a parent's false.
56
91
  */
57
92
  declare function hylePage(options: HyleNextOptions): (props: PageProps) => Promise<ReactElement>;
58
93
 
94
+ type HyleSitemapOptions = HyleNextOptions & {
95
+ /** The site's public origin, for example "https://tdacorp.in": https, no path, query or credentials. */
96
+ readonly origin: string;
97
+ /** Section slugs to include besides the default section (their index must be `basePath/<slug>`). */
98
+ readonly sections?: readonly string[];
99
+ /**
100
+ * What to do when hyle fails or a listing is cut off. `"partial"`: return what was collected. `"throw"`: throw, so a build
101
+ * during a hyle outage fails instead of shipping a partial sitemap. Default: throw only when nothing was collected.
102
+ */
103
+ readonly onIncomplete?: "partial" | "throw";
104
+ };
105
+ /**
106
+ * Sitemap entries for the mount, to spread into the host's `app/sitemap.ts`: `[...staticEntries, ...(await hyleSitemap({ ...sameOptionsAsHylePage, origin }))]`.
107
+ * Uses the same resolver (so the same cache and tags) as `hylePage`, and `localePrefix` for the URLs. `locale` is used only
108
+ * when it is a string: a function of the route params is never called here, so the sitemap is for the default locale.
109
+ * It pages each section index through `listPublicationPaths`, so only the default section and the listed `sections` appear,
110
+ * at most 2000 pages per section; paths are percent-encoded and `lastModified` is the publish time (omitted if it is not an
111
+ * ISO date). An invalid `origin` throws; see `onIncomplete` for hyle failures.
112
+ */
113
+ declare function hyleSitemap(options: HyleSitemapOptions): Promise<MetadataRoute.Sitemap>;
114
+
59
115
  /**
60
116
  * `export const generateMetadata = hyleMetadata({ ... })` with the same options as `hylePage`. Title, description, a canonical
61
117
  * URL in the page's own locale (so a fallback copy under another locale prefix points at the original), hreflang alternates
62
118
  * from the response's `alternates`, and Open Graph. Paths are relative: set `metadataBase` in the host's root layout.
63
- * Redirects, 404s and a switched-off read surface produce no metadata; `hylePage` handles those.
119
+ * Redirects, 404s and a switched-off read surface produce no metadata; `hylePage` handles those. A 4xx rejection of the request
120
+ * gives no metadata; any other failure (network, timeout, 5xx, 401, 403, 429) is rethrown, so a transient outage is not
121
+ * cached as a page; Next control flow is rethrown.
64
122
  */
65
123
  declare function hyleMetadata(options: HyleNextOptions): (props: PageProps) => Promise<Metadata>;
66
124
 
@@ -81,4 +139,4 @@ interface RevalidateRouteOptions {
81
139
  */
82
140
  declare function createRevalidateRoute(options: RevalidateRouteOptions): RevalidationCallbackRoute;
83
141
 
84
- export { FetchResolverOptions, type HyleNextOptions, HyleResolver, type PageProps, type RevalidateRouteOptions, type RouteParams, createRevalidateRoute, hyleMetadata, hylePage };
142
+ export { FetchResolverOptions, type HyleNextOptions, HyleResolver, type HyleSitemapOptions, type PageProps, type RevalidateRouteOptions, type RouteParams, createRevalidateRoute, hyleMetadata, hylePage, hyleSitemap };
package/dist/next.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { H as HyleComponents, a as HyleSlots } from './types-BNaGecO7.js';
3
- import { F as FetchResolverOptions, H as HyleResolver } from './resolve-Cz-3Q7T-.js';
4
- import { Metadata } from 'next';
3
+ import { F as FetchResolverOptions, H as HyleResolver } from './resolve-fMx2W8RR.js';
4
+ export { f as fetchResolver, w as workspaceTags } from './resolve-fMx2W8RR.js';
5
+ import { MetadataRoute, Metadata } from 'next';
5
6
  import { a as RevalidationCallbackRoute } from './revalidation-callback-D7Ms_Puz.js';
6
7
  import './types-DjXf6iPo.js';
7
8
 
@@ -28,6 +29,33 @@ interface CommonOptions {
28
29
  readonly layout?: "article" | "docs";
29
30
  /** Items per list page (server default 20, maximum 100). */
30
31
  readonly listLimit?: number;
32
+ /**
33
+ * Heading and description of the list page (the mount path's index). hyle names the default section after its slug
34
+ * ("Posts"); `list.title` replaces that in the page's h1, the `<title>` and Open Graph, and `list.description` fills the
35
+ * page description, `<meta name="description">` and Open Graph. Each falls back separately: title to the hyle section
36
+ * title, description to none. Article pages are unaffected.
37
+ */
38
+ readonly list?: {
39
+ readonly title?: string;
40
+ readonly description?: string;
41
+ };
42
+ /**
43
+ * `"searchParams"` makes list pages paginate: `?cursor=` is read, sent to hyle, and an "Older posts" link is built from
44
+ * `next_cursor`. Default off, and the trade-off is real: reading `searchParams` makes the route dynamic, so it can no longer
45
+ * be statically generated or served from ISR (hyle's fetch cache still applies). Leave it off when one page of `listLimit`
46
+ * items is enough. A malformed `cursor` is ignored (the first page is shown); one that is well-formed but unknown to hyle is a 404.
47
+ */
48
+ readonly paginate?: "searchParams";
49
+ /**
50
+ * Opt-in custom UI for transient or operational hyle failures (network error, timeout, 5xx, 401, 403, 429, a malformed
51
+ * response, a refused redirect target). Without it `hylePage` rethrows them, so Next keeps serving the last good ISR page
52
+ * when a revalidation fails (and a failure on a first render is an uncached 500). With it, the returned element is a NORMAL
53
+ * render: it is cached for the revalidate window and replaces the last good page for that long, so prefer a boundary
54
+ * (`error.tsx`) unless you want that. The `error` can carry `Cp1ClientError.message` and `requestId`: never render it.
55
+ * Only its name is logged. A 4xx rejection of the request (400, 404, ...) is always `notFound()` and never reaches this, and
56
+ * `notFound()` and redirects are Next control flow that always propagate.
57
+ */
58
+ readonly onError?: (error: unknown) => ReactElement;
31
59
  }
32
60
  /**
33
61
  * Pass either the fetch-cache settings (works with no host configuration) or a `resolver` from
@@ -40,6 +68,8 @@ type HyleNextOptions = CommonOptions & (({
40
68
  });
41
69
  interface PageProps {
42
70
  readonly params?: Promise<RouteParams>;
71
+ /** Read only with `paginate: "searchParams"`. */
72
+ readonly searchParams?: Promise<Record<string, string | string[] | undefined>>;
43
73
  }
44
74
 
45
75
  /**
@@ -50,17 +80,45 @@ interface PageProps {
50
80
  * in front, and 301 maps to permanentRedirect (308) while 307 maps to redirect (temporary). A target equal to the current
51
81
  * URL would loop, so it is treated as not found.
52
82
  *
83
+ * Failure: a 4xx rejection of the request (400, 404, ...) is `notFound()`. Every other failure (network error, timeout, 5xx,
84
+ * 401, 403, 429, a malformed response, a refused redirect target) is RETHROWN, so under ISR Next keeps serving the last good
85
+ * page when a revalidation fails, and a first-render failure is an uncached 500 rather than a cached soft 200. `onError`
86
+ * opts in to a custom element instead (see its docs). `notFound()` and redirects always propagate (`unstable_rethrow`).
87
+ *
53
88
  * Host requirement: no parent layout or page above the mount may export `dynamicParams = false`. Next inherits it, the
54
89
  * route is compiled with `"fallback": false` and every article not known at build time is a hard 404 (it falls through
55
90
  * to a `[locale]/[...rest]` catch-all). A page-level `dynamicParams = true` does not override a parent's false.
56
91
  */
57
92
  declare function hylePage(options: HyleNextOptions): (props: PageProps) => Promise<ReactElement>;
58
93
 
94
+ type HyleSitemapOptions = HyleNextOptions & {
95
+ /** The site's public origin, for example "https://tdacorp.in": https, no path, query or credentials. */
96
+ readonly origin: string;
97
+ /** Section slugs to include besides the default section (their index must be `basePath/<slug>`). */
98
+ readonly sections?: readonly string[];
99
+ /**
100
+ * What to do when hyle fails or a listing is cut off. `"partial"`: return what was collected. `"throw"`: throw, so a build
101
+ * during a hyle outage fails instead of shipping a partial sitemap. Default: throw only when nothing was collected.
102
+ */
103
+ readonly onIncomplete?: "partial" | "throw";
104
+ };
105
+ /**
106
+ * Sitemap entries for the mount, to spread into the host's `app/sitemap.ts`: `[...staticEntries, ...(await hyleSitemap({ ...sameOptionsAsHylePage, origin }))]`.
107
+ * Uses the same resolver (so the same cache and tags) as `hylePage`, and `localePrefix` for the URLs. `locale` is used only
108
+ * when it is a string: a function of the route params is never called here, so the sitemap is for the default locale.
109
+ * It pages each section index through `listPublicationPaths`, so only the default section and the listed `sections` appear,
110
+ * at most 2000 pages per section; paths are percent-encoded and `lastModified` is the publish time (omitted if it is not an
111
+ * ISO date). An invalid `origin` throws; see `onIncomplete` for hyle failures.
112
+ */
113
+ declare function hyleSitemap(options: HyleSitemapOptions): Promise<MetadataRoute.Sitemap>;
114
+
59
115
  /**
60
116
  * `export const generateMetadata = hyleMetadata({ ... })` with the same options as `hylePage`. Title, description, a canonical
61
117
  * URL in the page's own locale (so a fallback copy under another locale prefix points at the original), hreflang alternates
62
118
  * from the response's `alternates`, and Open Graph. Paths are relative: set `metadataBase` in the host's root layout.
63
- * Redirects, 404s and a switched-off read surface produce no metadata; `hylePage` handles those.
119
+ * Redirects, 404s and a switched-off read surface produce no metadata; `hylePage` handles those. A 4xx rejection of the request
120
+ * gives no metadata; any other failure (network, timeout, 5xx, 401, 403, 429) is rethrown, so a transient outage is not
121
+ * cached as a page; Next control flow is rethrown.
64
122
  */
65
123
  declare function hyleMetadata(options: HyleNextOptions): (props: PageProps) => Promise<Metadata>;
66
124
 
@@ -81,4 +139,4 @@ interface RevalidateRouteOptions {
81
139
  */
82
140
  declare function createRevalidateRoute(options: RevalidateRouteOptions): RevalidationCallbackRoute;
83
141
 
84
- export { FetchResolverOptions, type HyleNextOptions, HyleResolver, type PageProps, type RevalidateRouteOptions, type RouteParams, createRevalidateRoute, hyleMetadata, hylePage };
142
+ export { FetchResolverOptions, type HyleNextOptions, HyleResolver, type HyleSitemapOptions, type PageProps, type RevalidateRouteOptions, type RouteParams, createRevalidateRoute, hyleMetadata, hylePage, hyleSitemap };