qlara 0.1.10 → 0.1.11

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/aws.cjs CHANGED
@@ -599,7 +599,9 @@ var STACK_NAME_PREFIX = "qlara";
599
599
  var import_node_fs3 = require("fs");
600
600
  var import_node_path3 = require("path");
601
601
  var FALLBACK_FILENAME = "_fallback.html";
602
- var FALLBACK_PLACEHOLDER = "__QLARA_FALLBACK__";
602
+ function paramPlaceholder(paramName) {
603
+ return `__QLARA_FALLBACK_${paramName}__`;
604
+ }
603
605
  function generateFallbackFromTemplate(templateHtml, routePattern) {
604
606
  let fallback = templateHtml;
605
607
  const paramNames = (routePattern.match(/:([^/]+)/g) || []).map((m) => m.slice(1));
@@ -622,7 +624,7 @@ function generateFallbackFromTemplate(templateHtml, routePattern) {
622
624
  );
623
625
  fallback = fallback.replace(
624
626
  propsRegex,
625
- `{\\"${param}\\":\\"${FALLBACK_PLACEHOLDER}\\"}`
627
+ `{\\"${param}\\":\\"${paramPlaceholder(param)}\\"}`
626
628
  );
627
629
  }
628
630
  for (const param of paramNames) {
@@ -632,20 +634,30 @@ function generateFallbackFromTemplate(templateHtml, routePattern) {
632
634
  );
633
635
  fallback = fallback.replace(
634
636
  segmentRegex,
635
- `[\\"${param}\\",\\"${FALLBACK_PLACEHOLDER}\\",\\"d\\"]`
637
+ `[\\"${param}\\",\\"${paramPlaceholder(param)}\\",\\"d\\"]`
636
638
  );
637
639
  }
638
- const routeParts = routePattern.split("/").filter((p) => !p.startsWith(":"));
639
- if (routeParts.length > 0) {
640
- const prefix = routeParts.map((p) => `${q}${p}${q}`).join(",");
641
- const urlSegmentRegex = new RegExp(
642
- `(${q}c${q}:\\[${prefix},)${q}[^"]*${q}\\]`,
640
+ const allSegments = routePattern.split("/");
641
+ if (allSegments.length > 1) {
642
+ const regexParts = allSegments.map((seg) => {
643
+ if (seg.startsWith(":")) {
644
+ return `${q}[^"]*${q}`;
645
+ }
646
+ return `${q}${seg}${q}`;
647
+ });
648
+ const cArrayRegex = new RegExp(
649
+ `(${q}c${q}:\\[)${regexParts.join(",")}(\\])`,
643
650
  "g"
644
651
  );
645
- fallback = fallback.replace(
646
- urlSegmentRegex,
647
- `$1\\"${FALLBACK_PLACEHOLDER}\\"]`
648
- );
652
+ const replacementParts = allSegments.map((seg) => {
653
+ if (seg.startsWith(":")) {
654
+ const pName = seg.slice(1);
655
+ return `\\"${paramPlaceholder(pName)}\\"`;
656
+ }
657
+ return `\\"${seg}\\"`;
658
+ });
659
+ const replacement = `$1${replacementParts.join(",")}$2`;
660
+ fallback = fallback.replace(cArrayRegex, replacement);
649
661
  }
650
662
  fallback = fallback.replace(
651
663
  /8:\{\\"metadata\\":\[[\s\S]*?\],\\"error\\":null,\\"digest\\":\\"?\$undefined\\?"\}/,
package/dist/aws.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as ProviderResources, Q as QlaraProvider } from './types--KPPgCtc.cjs';
1
+ import { P as ProviderResources, Q as QlaraProvider } from './types-BmSR1R_Q.cjs';
2
2
 
3
3
  interface AwsConfig {
4
4
  stackName?: string;
package/dist/aws.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as ProviderResources, Q as QlaraProvider } from './types--KPPgCtc.js';
1
+ import { P as ProviderResources, Q as QlaraProvider } from './types-BmSR1R_Q.js';
2
2
 
3
3
  interface AwsConfig {
4
4
  stackName?: string;
package/dist/aws.js CHANGED
@@ -595,7 +595,9 @@ var STACK_NAME_PREFIX = "qlara";
595
595
  import { readFileSync as readFileSync3, writeFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
596
596
  import { join as join3 } from "path";
597
597
  var FALLBACK_FILENAME = "_fallback.html";
598
- var FALLBACK_PLACEHOLDER = "__QLARA_FALLBACK__";
598
+ function paramPlaceholder(paramName) {
599
+ return `__QLARA_FALLBACK_${paramName}__`;
600
+ }
599
601
  function generateFallbackFromTemplate(templateHtml, routePattern) {
600
602
  let fallback = templateHtml;
601
603
  const paramNames = (routePattern.match(/:([^/]+)/g) || []).map((m) => m.slice(1));
@@ -618,7 +620,7 @@ function generateFallbackFromTemplate(templateHtml, routePattern) {
618
620
  );
619
621
  fallback = fallback.replace(
620
622
  propsRegex,
621
- `{\\"${param}\\":\\"${FALLBACK_PLACEHOLDER}\\"}`
623
+ `{\\"${param}\\":\\"${paramPlaceholder(param)}\\"}`
622
624
  );
623
625
  }
624
626
  for (const param of paramNames) {
@@ -628,20 +630,30 @@ function generateFallbackFromTemplate(templateHtml, routePattern) {
628
630
  );
629
631
  fallback = fallback.replace(
630
632
  segmentRegex,
631
- `[\\"${param}\\",\\"${FALLBACK_PLACEHOLDER}\\",\\"d\\"]`
633
+ `[\\"${param}\\",\\"${paramPlaceholder(param)}\\",\\"d\\"]`
632
634
  );
633
635
  }
634
- const routeParts = routePattern.split("/").filter((p) => !p.startsWith(":"));
635
- if (routeParts.length > 0) {
636
- const prefix = routeParts.map((p) => `${q}${p}${q}`).join(",");
637
- const urlSegmentRegex = new RegExp(
638
- `(${q}c${q}:\\[${prefix},)${q}[^"]*${q}\\]`,
636
+ const allSegments = routePattern.split("/");
637
+ if (allSegments.length > 1) {
638
+ const regexParts = allSegments.map((seg) => {
639
+ if (seg.startsWith(":")) {
640
+ return `${q}[^"]*${q}`;
641
+ }
642
+ return `${q}${seg}${q}`;
643
+ });
644
+ const cArrayRegex = new RegExp(
645
+ `(${q}c${q}:\\[)${regexParts.join(",")}(\\])`,
639
646
  "g"
640
647
  );
641
- fallback = fallback.replace(
642
- urlSegmentRegex,
643
- `$1\\"${FALLBACK_PLACEHOLDER}\\"]`
644
- );
648
+ const replacementParts = allSegments.map((seg) => {
649
+ if (seg.startsWith(":")) {
650
+ const pName = seg.slice(1);
651
+ return `\\"${paramPlaceholder(pName)}\\"`;
652
+ }
653
+ return `\\"${seg}\\"`;
654
+ });
655
+ const replacement = `$1${replacementParts.join(",")}$2`;
656
+ fallback = fallback.replace(cArrayRegex, replacement);
645
657
  }
646
658
  fallback = fallback.replace(
647
659
  /8:\{\\"metadata\\":\[[\s\S]*?\],\\"error\\":null,\\"digest\\":\\"?\$undefined\\?"\}/,
package/dist/cli.js CHANGED
@@ -603,7 +603,9 @@ async function bundleRenderer(routeFile, cacheTtl = 3600, framework) {
603
603
  import { readFileSync as readFileSync3, writeFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
604
604
  import { join as join3 } from "path";
605
605
  var FALLBACK_FILENAME = "_fallback.html";
606
- var FALLBACK_PLACEHOLDER = "__QLARA_FALLBACK__";
606
+ function paramPlaceholder(paramName) {
607
+ return `__QLARA_FALLBACK_${paramName}__`;
608
+ }
607
609
  function generateFallbackFromTemplate(templateHtml, routePattern) {
608
610
  let fallback = templateHtml;
609
611
  const paramNames = (routePattern.match(/:([^/]+)/g) || []).map((m) => m.slice(1));
@@ -626,7 +628,7 @@ function generateFallbackFromTemplate(templateHtml, routePattern) {
626
628
  );
627
629
  fallback = fallback.replace(
628
630
  propsRegex,
629
- `{\\"${param}\\":\\"${FALLBACK_PLACEHOLDER}\\"}`
631
+ `{\\"${param}\\":\\"${paramPlaceholder(param)}\\"}`
630
632
  );
631
633
  }
632
634
  for (const param of paramNames) {
@@ -636,20 +638,30 @@ function generateFallbackFromTemplate(templateHtml, routePattern) {
636
638
  );
637
639
  fallback = fallback.replace(
638
640
  segmentRegex,
639
- `[\\"${param}\\",\\"${FALLBACK_PLACEHOLDER}\\",\\"d\\"]`
641
+ `[\\"${param}\\",\\"${paramPlaceholder(param)}\\",\\"d\\"]`
640
642
  );
641
643
  }
642
- const routeParts = routePattern.split("/").filter((p) => !p.startsWith(":"));
643
- if (routeParts.length > 0) {
644
- const prefix = routeParts.map((p) => `${q}${p}${q}`).join(",");
645
- const urlSegmentRegex = new RegExp(
646
- `(${q}c${q}:\\[${prefix},)${q}[^"]*${q}\\]`,
644
+ const allSegments = routePattern.split("/");
645
+ if (allSegments.length > 1) {
646
+ const regexParts = allSegments.map((seg) => {
647
+ if (seg.startsWith(":")) {
648
+ return `${q}[^"]*${q}`;
649
+ }
650
+ return `${q}${seg}${q}`;
651
+ });
652
+ const cArrayRegex = new RegExp(
653
+ `(${q}c${q}:\\[)${regexParts.join(",")}(\\])`,
647
654
  "g"
648
655
  );
649
- fallback = fallback.replace(
650
- urlSegmentRegex,
651
- `$1\\"${FALLBACK_PLACEHOLDER}\\"]`
652
- );
656
+ const replacementParts = allSegments.map((seg) => {
657
+ if (seg.startsWith(":")) {
658
+ const pName = seg.slice(1);
659
+ return `\\"${paramPlaceholder(pName)}\\"`;
660
+ }
661
+ return `\\"${seg}\\"`;
662
+ });
663
+ const replacement = `$1${replacementParts.join(",")}$2`;
664
+ fallback = fallback.replace(cArrayRegex, replacement);
653
665
  }
654
666
  fallback = fallback.replace(
655
667
  /8:\{\\"metadata\\":\[[\s\S]*?\],\\"error\\":null,\\"digest\\":\\"?\$undefined\\?"\}/,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types--KPPgCtc.cjs';
2
- export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraVerification } from './types--KPPgCtc.cjs';
1
+ import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types-BmSR1R_Q.cjs';
2
+ export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraValidate, aa as QlaraVerification } from './types-BmSR1R_Q.cjs';
3
3
 
4
4
  declare function validateConfig(config: QlaraPluginConfig, routes: QlaraRoute[]): void;
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types--KPPgCtc.js';
2
- export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraVerification } from './types--KPPgCtc.js';
1
+ import { a as QlaraPluginConfig, b as QlaraRoute, c as QlaraManifest, M as ManifestRoute, R as RouteMatch } from './types-BmSR1R_Q.js';
2
+ export { P as ProviderResources, d as QlaraAlternateLinkDescriptor, e as QlaraAlternateURLs, f as QlaraAppLinks, g as QlaraAppLinksAndroid, h as QlaraAppLinksApple, i as QlaraAppLinksWeb, j as QlaraAppLinksWindows, k as QlaraAppleImage, l as QlaraAppleImageDescriptor, m as QlaraAppleWebApp, n as QlaraAuthor, o as QlaraDeployConfig, p as QlaraFacebook, q as QlaraFormatDetection, r as QlaraIcon, s as QlaraIconDescriptor, t as QlaraIcons, u as QlaraItunesApp, v as QlaraMetaDataGenerator, w as QlaraMetadata, x as QlaraOGAudio, y as QlaraOGAudioDescriptor, z as QlaraOGImage, A as QlaraOGImageDescriptor, B as QlaraOGVideo, C as QlaraOGVideoDescriptor, D as QlaraOpenGraph, E as QlaraOpenGraphArticle, F as QlaraOpenGraphBase, G as QlaraOpenGraphBook, H as QlaraOpenGraphMusicAlbum, I as QlaraOpenGraphMusicPlaylist, J as QlaraOpenGraphMusicRadioStation, K as QlaraOpenGraphMusicSong, L as QlaraOpenGraphProfile, N as QlaraOpenGraphVideoEpisode, O as QlaraOpenGraphVideoMovie, S as QlaraOpenGraphVideoOther, T as QlaraOpenGraphVideoTVShow, U as QlaraOpenGraphWebsite, V as QlaraPinterest, Q as QlaraProvider, W as QlaraReferrer, X as QlaraRobots, Y as QlaraRobotsInfo, Z as QlaraRouteDefinition, _ as QlaraRoutes, $ as QlaraTwitter, a0 as QlaraTwitterApp, a1 as QlaraTwitterAppDescriptor, a2 as QlaraTwitterBase, a3 as QlaraTwitterImage, a4 as QlaraTwitterImageDescriptor, a5 as QlaraTwitterPlayer, a6 as QlaraTwitterPlayerDescriptor, a7 as QlaraTwitterSummary, a8 as QlaraTwitterSummaryLargeImage, a9 as QlaraValidate, aa as QlaraVerification } from './types-BmSR1R_Q.js';
3
3
 
4
4
  declare function validateConfig(config: QlaraPluginConfig, routes: QlaraRoute[]): void;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { NextConfig } from 'next';
2
- import { a as QlaraPluginConfig } from '../types--KPPgCtc.cjs';
2
+ import { a as QlaraPluginConfig } from '../types-BmSR1R_Q.cjs';
3
3
 
4
4
  /**
5
5
  * Wrap a Next.js config with Qlara.
@@ -1,5 +1,5 @@
1
1
  import { NextConfig } from 'next';
2
- import { a as QlaraPluginConfig } from '../types--KPPgCtc.js';
2
+ import { a as QlaraPluginConfig } from '../types-BmSR1R_Q.js';
3
3
 
4
4
  /**
5
5
  * Wrap a Next.js config with Qlara.
@@ -315,31 +315,60 @@ interface QlaraMetadata {
315
315
  other?: Record<string, string | number | (string | number)[]>;
316
316
  }
317
317
  /**
318
- * Function that fetches data for a dynamic route and returns metadata.
318
+ * Function that generates metadata for a dynamic route.
319
319
  * Equivalent to Next.js `generateMetadata()` — runs in the renderer Lambda
320
320
  * with access to the data source.
321
321
  *
322
- * @param params - The route parameters, e.g. { id: '42' } for /product/:id
322
+ * @param params - All route parameters, e.g. { lang: 'en', id: '42' } for /:lang/products/:id
323
323
  * @returns Metadata for the page, or null if the page doesn't exist
324
324
  */
325
325
  type QlaraMetaDataGenerator = (params: Record<string, string>) => Promise<QlaraMetadata | null>;
326
- /** A single route definition with its pattern and metadata generator. */
326
+ /**
327
+ * Optional validation function for a dynamic route.
328
+ * Called before reading the fallback or generating metadata.
329
+ * Use this to cheaply reject invalid param combinations (e.g., unsupported languages).
330
+ *
331
+ * Receives ALL route parameters — you can validate every dynamic segment.
332
+ *
333
+ * @param params - All route parameters, e.g. { lang: 'en', id: '42' }
334
+ * @returns true if the params are valid, false to return 404 immediately
335
+ */
336
+ type QlaraValidate = (params: Record<string, string>) => Promise<boolean>;
337
+ /**
338
+ * A single route definition at the leaf page level.
339
+ *
340
+ * Follows the Next.js "generate from bottom up" pattern:
341
+ * define one route entry per leaf page with ALL dynamic params.
342
+ */
327
343
  interface QlaraRouteDefinition {
328
- /** Dynamic route pattern, e.g. '/product/:id' */
344
+ /** Dynamic route pattern, e.g. '/:lang/products/:id' */
329
345
  route: string;
330
- /** Function that fetches metadata for this route from the data source */
331
- metaDataGenerator: QlaraMetaDataGenerator;
346
+ /**
347
+ * Optional validation function — called before rendering.
348
+ * Return `false` to 404 immediately without generating the page.
349
+ * Receives all params so you can validate every dynamic segment.
350
+ */
351
+ validate?: QlaraValidate;
352
+ /**
353
+ * Function that generates metadata for this route.
354
+ * Preferred name — use this instead of `metaDataGenerator`.
355
+ */
356
+ generateMetadata?: QlaraMetaDataGenerator;
357
+ /**
358
+ * @deprecated Use `generateMetadata` instead. Kept for backward compatibility.
359
+ */
360
+ metaDataGenerator?: QlaraMetaDataGenerator;
332
361
  }
333
362
  /**
334
363
  * The route file default export type: an array of route definitions.
335
364
  *
336
- * Example:
365
+ * Example (single param):
337
366
  * ```typescript
338
367
  * import type { QlaraRoutes } from 'qlara';
339
368
  * const routes: QlaraRoutes = [
340
369
  * {
341
370
  * route: '/product/:id',
342
- * metaDataGenerator: async (params) => {
371
+ * generateMetadata: async (params) => {
343
372
  * const product = await getProduct(params.id);
344
373
  * if (!product) return null;
345
374
  * return { title: product.name, description: product.description };
@@ -348,6 +377,25 @@ interface QlaraRouteDefinition {
348
377
  * ];
349
378
  * export default routes;
350
379
  * ```
380
+ *
381
+ * Example (multiple params with validation):
382
+ * ```typescript
383
+ * const routes: QlaraRoutes = [
384
+ * {
385
+ * route: '/:lang/products/:id',
386
+ * validate: async (params) => {
387
+ * if (!['en', 'da', 'de'].includes(params.lang)) return false;
388
+ * const product = await getProduct(params.id);
389
+ * return !!product;
390
+ * },
391
+ * generateMetadata: async (params) => {
392
+ * const product = await getProduct(params.id);
393
+ * if (!product) return null;
394
+ * return { title: `${product.name} | Store` };
395
+ * },
396
+ * },
397
+ * ];
398
+ * ```
351
399
  */
352
400
  type QlaraRoutes = QlaraRouteDefinition[];
353
401
  interface QlaraRoute {
@@ -420,4 +468,4 @@ interface RouteMatch {
420
468
  params: Record<string, string>;
421
469
  }
422
470
 
423
- export type { QlaraTwitter as $, QlaraOGImageDescriptor as A, QlaraOGVideo as B, QlaraOGVideoDescriptor as C, QlaraOpenGraph as D, QlaraOpenGraphArticle as E, QlaraOpenGraphBase as F, QlaraOpenGraphBook as G, QlaraOpenGraphMusicAlbum as H, QlaraOpenGraphMusicPlaylist as I, QlaraOpenGraphMusicRadioStation as J, QlaraOpenGraphMusicSong as K, QlaraOpenGraphProfile as L, ManifestRoute as M, QlaraOpenGraphVideoEpisode as N, QlaraOpenGraphVideoMovie as O, ProviderResources as P, QlaraProvider as Q, RouteMatch as R, QlaraOpenGraphVideoOther as S, QlaraOpenGraphVideoTVShow as T, QlaraOpenGraphWebsite as U, QlaraPinterest as V, QlaraReferrer as W, QlaraRobots as X, QlaraRobotsInfo as Y, QlaraRouteDefinition as Z, QlaraRoutes as _, QlaraPluginConfig as a, QlaraTwitterApp as a0, QlaraTwitterAppDescriptor as a1, QlaraTwitterBase as a2, QlaraTwitterImage as a3, QlaraTwitterImageDescriptor as a4, QlaraTwitterPlayer as a5, QlaraTwitterPlayerDescriptor as a6, QlaraTwitterSummary as a7, QlaraTwitterSummaryLargeImage as a8, QlaraVerification as a9, QlaraRoute as b, QlaraManifest as c, QlaraAlternateLinkDescriptor as d, QlaraAlternateURLs as e, QlaraAppLinks as f, QlaraAppLinksAndroid as g, QlaraAppLinksApple as h, QlaraAppLinksWeb as i, QlaraAppLinksWindows as j, QlaraAppleImage as k, QlaraAppleImageDescriptor as l, QlaraAppleWebApp as m, QlaraAuthor as n, QlaraDeployConfig as o, QlaraFacebook as p, QlaraFormatDetection as q, QlaraIcon as r, QlaraIconDescriptor as s, QlaraIcons as t, QlaraItunesApp as u, QlaraMetaDataGenerator as v, QlaraMetadata as w, QlaraOGAudio as x, QlaraOGAudioDescriptor as y, QlaraOGImage as z };
471
+ export type { QlaraTwitter as $, QlaraOGImageDescriptor as A, QlaraOGVideo as B, QlaraOGVideoDescriptor as C, QlaraOpenGraph as D, QlaraOpenGraphArticle as E, QlaraOpenGraphBase as F, QlaraOpenGraphBook as G, QlaraOpenGraphMusicAlbum as H, QlaraOpenGraphMusicPlaylist as I, QlaraOpenGraphMusicRadioStation as J, QlaraOpenGraphMusicSong as K, QlaraOpenGraphProfile as L, ManifestRoute as M, QlaraOpenGraphVideoEpisode as N, QlaraOpenGraphVideoMovie as O, ProviderResources as P, QlaraProvider as Q, RouteMatch as R, QlaraOpenGraphVideoOther as S, QlaraOpenGraphVideoTVShow as T, QlaraOpenGraphWebsite as U, QlaraPinterest as V, QlaraReferrer as W, QlaraRobots as X, QlaraRobotsInfo as Y, QlaraRouteDefinition as Z, QlaraRoutes as _, QlaraPluginConfig as a, QlaraTwitterApp as a0, QlaraTwitterAppDescriptor as a1, QlaraTwitterBase as a2, QlaraTwitterImage as a3, QlaraTwitterImageDescriptor as a4, QlaraTwitterPlayer as a5, QlaraTwitterPlayerDescriptor as a6, QlaraTwitterSummary as a7, QlaraTwitterSummaryLargeImage as a8, QlaraValidate as a9, QlaraVerification as aa, QlaraRoute as b, QlaraManifest as c, QlaraAlternateLinkDescriptor as d, QlaraAlternateURLs as e, QlaraAppLinks as f, QlaraAppLinksAndroid as g, QlaraAppLinksApple as h, QlaraAppLinksWeb as i, QlaraAppLinksWindows as j, QlaraAppleImage as k, QlaraAppleImageDescriptor as l, QlaraAppleWebApp as m, QlaraAuthor as n, QlaraDeployConfig as o, QlaraFacebook as p, QlaraFormatDetection as q, QlaraIcon as r, QlaraIconDescriptor as s, QlaraIcons as t, QlaraItunesApp as u, QlaraMetaDataGenerator as v, QlaraMetadata as w, QlaraOGAudio as x, QlaraOGAudioDescriptor as y, QlaraOGImage as z };
@@ -315,31 +315,60 @@ interface QlaraMetadata {
315
315
  other?: Record<string, string | number | (string | number)[]>;
316
316
  }
317
317
  /**
318
- * Function that fetches data for a dynamic route and returns metadata.
318
+ * Function that generates metadata for a dynamic route.
319
319
  * Equivalent to Next.js `generateMetadata()` — runs in the renderer Lambda
320
320
  * with access to the data source.
321
321
  *
322
- * @param params - The route parameters, e.g. { id: '42' } for /product/:id
322
+ * @param params - All route parameters, e.g. { lang: 'en', id: '42' } for /:lang/products/:id
323
323
  * @returns Metadata for the page, or null if the page doesn't exist
324
324
  */
325
325
  type QlaraMetaDataGenerator = (params: Record<string, string>) => Promise<QlaraMetadata | null>;
326
- /** A single route definition with its pattern and metadata generator. */
326
+ /**
327
+ * Optional validation function for a dynamic route.
328
+ * Called before reading the fallback or generating metadata.
329
+ * Use this to cheaply reject invalid param combinations (e.g., unsupported languages).
330
+ *
331
+ * Receives ALL route parameters — you can validate every dynamic segment.
332
+ *
333
+ * @param params - All route parameters, e.g. { lang: 'en', id: '42' }
334
+ * @returns true if the params are valid, false to return 404 immediately
335
+ */
336
+ type QlaraValidate = (params: Record<string, string>) => Promise<boolean>;
337
+ /**
338
+ * A single route definition at the leaf page level.
339
+ *
340
+ * Follows the Next.js "generate from bottom up" pattern:
341
+ * define one route entry per leaf page with ALL dynamic params.
342
+ */
327
343
  interface QlaraRouteDefinition {
328
- /** Dynamic route pattern, e.g. '/product/:id' */
344
+ /** Dynamic route pattern, e.g. '/:lang/products/:id' */
329
345
  route: string;
330
- /** Function that fetches metadata for this route from the data source */
331
- metaDataGenerator: QlaraMetaDataGenerator;
346
+ /**
347
+ * Optional validation function — called before rendering.
348
+ * Return `false` to 404 immediately without generating the page.
349
+ * Receives all params so you can validate every dynamic segment.
350
+ */
351
+ validate?: QlaraValidate;
352
+ /**
353
+ * Function that generates metadata for this route.
354
+ * Preferred name — use this instead of `metaDataGenerator`.
355
+ */
356
+ generateMetadata?: QlaraMetaDataGenerator;
357
+ /**
358
+ * @deprecated Use `generateMetadata` instead. Kept for backward compatibility.
359
+ */
360
+ metaDataGenerator?: QlaraMetaDataGenerator;
332
361
  }
333
362
  /**
334
363
  * The route file default export type: an array of route definitions.
335
364
  *
336
- * Example:
365
+ * Example (single param):
337
366
  * ```typescript
338
367
  * import type { QlaraRoutes } from 'qlara';
339
368
  * const routes: QlaraRoutes = [
340
369
  * {
341
370
  * route: '/product/:id',
342
- * metaDataGenerator: async (params) => {
371
+ * generateMetadata: async (params) => {
343
372
  * const product = await getProduct(params.id);
344
373
  * if (!product) return null;
345
374
  * return { title: product.name, description: product.description };
@@ -348,6 +377,25 @@ interface QlaraRouteDefinition {
348
377
  * ];
349
378
  * export default routes;
350
379
  * ```
380
+ *
381
+ * Example (multiple params with validation):
382
+ * ```typescript
383
+ * const routes: QlaraRoutes = [
384
+ * {
385
+ * route: '/:lang/products/:id',
386
+ * validate: async (params) => {
387
+ * if (!['en', 'da', 'de'].includes(params.lang)) return false;
388
+ * const product = await getProduct(params.id);
389
+ * return !!product;
390
+ * },
391
+ * generateMetadata: async (params) => {
392
+ * const product = await getProduct(params.id);
393
+ * if (!product) return null;
394
+ * return { title: `${product.name} | Store` };
395
+ * },
396
+ * },
397
+ * ];
398
+ * ```
351
399
  */
352
400
  type QlaraRoutes = QlaraRouteDefinition[];
353
401
  interface QlaraRoute {
@@ -420,4 +468,4 @@ interface RouteMatch {
420
468
  params: Record<string, string>;
421
469
  }
422
470
 
423
- export type { QlaraTwitter as $, QlaraOGImageDescriptor as A, QlaraOGVideo as B, QlaraOGVideoDescriptor as C, QlaraOpenGraph as D, QlaraOpenGraphArticle as E, QlaraOpenGraphBase as F, QlaraOpenGraphBook as G, QlaraOpenGraphMusicAlbum as H, QlaraOpenGraphMusicPlaylist as I, QlaraOpenGraphMusicRadioStation as J, QlaraOpenGraphMusicSong as K, QlaraOpenGraphProfile as L, ManifestRoute as M, QlaraOpenGraphVideoEpisode as N, QlaraOpenGraphVideoMovie as O, ProviderResources as P, QlaraProvider as Q, RouteMatch as R, QlaraOpenGraphVideoOther as S, QlaraOpenGraphVideoTVShow as T, QlaraOpenGraphWebsite as U, QlaraPinterest as V, QlaraReferrer as W, QlaraRobots as X, QlaraRobotsInfo as Y, QlaraRouteDefinition as Z, QlaraRoutes as _, QlaraPluginConfig as a, QlaraTwitterApp as a0, QlaraTwitterAppDescriptor as a1, QlaraTwitterBase as a2, QlaraTwitterImage as a3, QlaraTwitterImageDescriptor as a4, QlaraTwitterPlayer as a5, QlaraTwitterPlayerDescriptor as a6, QlaraTwitterSummary as a7, QlaraTwitterSummaryLargeImage as a8, QlaraVerification as a9, QlaraRoute as b, QlaraManifest as c, QlaraAlternateLinkDescriptor as d, QlaraAlternateURLs as e, QlaraAppLinks as f, QlaraAppLinksAndroid as g, QlaraAppLinksApple as h, QlaraAppLinksWeb as i, QlaraAppLinksWindows as j, QlaraAppleImage as k, QlaraAppleImageDescriptor as l, QlaraAppleWebApp as m, QlaraAuthor as n, QlaraDeployConfig as o, QlaraFacebook as p, QlaraFormatDetection as q, QlaraIcon as r, QlaraIconDescriptor as s, QlaraIcons as t, QlaraItunesApp as u, QlaraMetaDataGenerator as v, QlaraMetadata as w, QlaraOGAudio as x, QlaraOGAudioDescriptor as y, QlaraOGImage as z };
471
+ export type { QlaraTwitter as $, QlaraOGImageDescriptor as A, QlaraOGVideo as B, QlaraOGVideoDescriptor as C, QlaraOpenGraph as D, QlaraOpenGraphArticle as E, QlaraOpenGraphBase as F, QlaraOpenGraphBook as G, QlaraOpenGraphMusicAlbum as H, QlaraOpenGraphMusicPlaylist as I, QlaraOpenGraphMusicRadioStation as J, QlaraOpenGraphMusicSong as K, QlaraOpenGraphProfile as L, ManifestRoute as M, QlaraOpenGraphVideoEpisode as N, QlaraOpenGraphVideoMovie as O, ProviderResources as P, QlaraProvider as Q, RouteMatch as R, QlaraOpenGraphVideoOther as S, QlaraOpenGraphVideoTVShow as T, QlaraOpenGraphWebsite as U, QlaraPinterest as V, QlaraReferrer as W, QlaraRobots as X, QlaraRobotsInfo as Y, QlaraRouteDefinition as Z, QlaraRoutes as _, QlaraPluginConfig as a, QlaraTwitterApp as a0, QlaraTwitterAppDescriptor as a1, QlaraTwitterBase as a2, QlaraTwitterImage as a3, QlaraTwitterImageDescriptor as a4, QlaraTwitterPlayer as a5, QlaraTwitterPlayerDescriptor as a6, QlaraTwitterSummary as a7, QlaraTwitterSummaryLargeImage as a8, QlaraValidate as a9, QlaraVerification as aa, QlaraRoute as b, QlaraManifest as c, QlaraAlternateLinkDescriptor as d, QlaraAlternateURLs as e, QlaraAppLinks as f, QlaraAppLinksAndroid as g, QlaraAppLinksApple as h, QlaraAppLinksWeb as i, QlaraAppLinksWindows as j, QlaraAppleImage as k, QlaraAppleImageDescriptor as l, QlaraAppleWebApp as m, QlaraAuthor as n, QlaraDeployConfig as o, QlaraFacebook as p, QlaraFormatDetection as q, QlaraIcon as r, QlaraIconDescriptor as s, QlaraIcons as t, QlaraItunesApp as u, QlaraMetaDataGenerator as v, QlaraMetadata as w, QlaraOGAudio as x, QlaraOGAudioDescriptor as y, QlaraOGImage as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qlara",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Runtime ISR for static React apps — dynamic routing and SEO metadata for statically exported Next.js apps on AWS",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -78,8 +78,6 @@ interface RendererResult {
78
78
  html?: string;
79
79
  }
80
80
 
81
- const FALLBACK_PLACEHOLDER = '__QLARA_FALLBACK__';
82
-
83
81
  // Module-scope S3 client — reused across warm invocations (avoids recreating TCP/TLS connections)
84
82
  const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
85
83
 
@@ -95,23 +93,27 @@ function deriveS3Key(uri: string): string {
95
93
  }
96
94
 
97
95
  /**
98
- * Derive the fallback S3 key from a URI.
99
- * /product/42 product/_fallback.html
96
+ * Derive the fallback S3 key from a route pattern.
97
+ * Filters out dynamic segments (starting with ':') and appends _fallback.html.
98
+ * Must match getFallbackKey() in fallback.ts.
99
+ *
100
+ * '/product/:id' → 'product/_fallback.html'
101
+ * '/:lang/products/:id' → 'products/_fallback.html'
102
+ * '/:a/:b/:c' → '_fallback.html'
100
103
  */
101
- function deriveFallbackKey(uri: string): string {
102
- const cleanUri = uri.replace(/^\//, '').replace(/\/$/, '');
103
- const parts = cleanUri.split('/');
104
- if (parts.length < 2) return '_fallback.html';
105
- return parts.slice(0, -1).join('/') + '/_fallback.html';
104
+ function deriveFallbackKey(routePattern: string): string {
105
+ const parts = routePattern.replace(/^\//, '').split('/');
106
+ const dirParts = parts.filter(p => !p.startsWith(':'));
107
+ return [...dirParts, '_fallback.html'].join('/');
106
108
  }
107
109
 
108
110
  /**
109
- * Extract the last path segment (the dynamic param value) from a URI.
110
- * /product/42 '42'
111
+ * Generate a per-param placeholder string.
112
+ * Must match paramPlaceholder() in fallback.ts (inlined because renderer
113
+ * is bundled as a standalone Lambda ZIP).
111
114
  */
112
- function extractParamValue(uri: string): string {
113
- const cleanUri = uri.replace(/^\//, '').replace(/\/$/, '');
114
- return cleanUri.split('/').pop() || '';
115
+ function paramPlaceholder(paramName: string): string {
116
+ return `__QLARA_FALLBACK_${paramName}__`;
115
117
  }
116
118
 
117
119
  // ── Helpers: normalize single-or-array values ────────────────────
@@ -866,13 +868,19 @@ async function listReferenceSegmentFiles(bucket: string, refDir: string): Promis
866
868
  }
867
869
 
868
870
  /**
869
- * Patch the _tree.txt segment: replace the dynamic paramKey value.
871
+ * Patch the _tree.txt segment: replace dynamic paramKey values for all params.
872
+ * Each dynamic segment has "name":"<paramName>","paramType":"d","paramKey":"<value>".
873
+ * We match on the param name to replace the correct paramKey for each param.
870
874
  */
871
- function patchTreeSegment(template: string, newValue: string): string {
872
- return template.replace(
873
- /"paramType":"d","paramKey":"[^"]*"/,
874
- `"paramType":"d","paramKey":"${newValue}"`
875
- );
875
+ function patchTreeSegment(template: string, params: Record<string, string>): string {
876
+ let result = template;
877
+ for (const [name, value] of Object.entries(params)) {
878
+ result = result.replace(
879
+ new RegExp(`"name":"${name}","paramType":"d","paramKey":"[^"]*"`),
880
+ `"name":"${name}","paramType":"d","paramKey":"${value}"`
881
+ );
882
+ }
883
+ return result;
876
884
  }
877
885
 
878
886
  /**
@@ -1033,7 +1041,6 @@ async function generateSegmentFiles(
1033
1041
  }
1034
1042
 
1035
1043
  // Generate each segment file
1036
- const paramValue = parts[parts.length - 1]; // last path segment = dynamic param value
1037
1044
  const uploads: Promise<unknown>[] = [];
1038
1045
  const cacheControl = `public, max-age=0, s-maxage=${__QLARA_CACHE_TTL__}, stale-while-revalidate=60`;
1039
1046
 
@@ -1048,7 +1055,7 @@ async function generateSegmentFiles(
1048
1055
  case 'tree': {
1049
1056
  const template = refMap.get(name);
1050
1057
  if (template) {
1051
- content = patchTreeSegment(template, paramValue);
1058
+ content = patchTreeSegment(template, params);
1052
1059
  }
1053
1060
  break;
1054
1061
  }
@@ -1108,9 +1115,22 @@ export async function handler(event: RendererEvent & { warmup?: boolean }): Prom
1108
1115
  const { uri, bucket, routePattern, params } = event;
1109
1116
 
1110
1117
  try {
1111
- // 0. Check if already rendered + read fallback in parallel
1118
+ // 0. Find route definition and run validation (if defined)
1119
+ const routeDef = routes?.find((r: { route: string }) => r.route === routePattern);
1120
+
1121
+ if (routeDef?.validate) {
1122
+ const isValid = await routeDef.validate(params);
1123
+ if (!isValid) {
1124
+ return {
1125
+ statusCode: 404,
1126
+ body: JSON.stringify({ error: `Validation failed for ${uri}`, params }),
1127
+ };
1128
+ }
1129
+ }
1130
+
1131
+ // 1. Check if already rendered + read fallback in parallel
1112
1132
  const s3Key = deriveS3Key(uri);
1113
- const fallbackKey = deriveFallbackKey(uri);
1133
+ const fallbackKey = deriveFallbackKey(routePattern);
1114
1134
 
1115
1135
  const [existingResult, fallbackResult] = await Promise.allSettled([
1116
1136
  s3.send(new GetObjectCommand({ Bucket: bucket, Key: s3Key })),
@@ -1147,19 +1167,18 @@ export async function handler(event: RendererEvent & { warmup?: boolean }): Prom
1147
1167
  };
1148
1168
  }
1149
1169
 
1150
- // 2. Patch the fallback with the actual param value
1151
- const paramValue = extractParamValue(uri);
1152
- let html = fallbackHtml.replace(
1153
- new RegExp(FALLBACK_PLACEHOLDER, 'g'),
1154
- paramValue
1155
- );
1170
+ // 2. Patch the fallback with actual param values (per-param placeholders)
1171
+ let html = fallbackHtml;
1172
+ for (const [name, value] of Object.entries(params)) {
1173
+ html = html.replace(new RegExp(paramPlaceholder(name), 'g'), value);
1174
+ }
1156
1175
 
1157
- // 3. Call the metaDataGenerator to fetch metadata from the data source
1158
- const routeDef = routes?.find((r: { route: string }) => r.route === routePattern);
1176
+ // 3. Call generateMetadata (or deprecated metaDataGenerator) to fetch metadata
1177
+ const metadataFn = routeDef?.generateMetadata || routeDef?.metaDataGenerator;
1159
1178
  let metadata: QlaraMetadata | null = null;
1160
1179
 
1161
- if (routeDef?.metaDataGenerator) {
1162
- metadata = await routeDef.metaDataGenerator(params);
1180
+ if (metadataFn) {
1181
+ metadata = await metadataFn(params);
1163
1182
  if (metadata) {
1164
1183
  // 4. Patch the HTML with real metadata
1165
1184
  html = patchMetadata(html, metadata);
package/src/types.ts CHANGED
@@ -456,33 +456,63 @@ export interface QlaraMetadata {
456
456
  }
457
457
 
458
458
  /**
459
- * Function that fetches data for a dynamic route and returns metadata.
459
+ * Function that generates metadata for a dynamic route.
460
460
  * Equivalent to Next.js `generateMetadata()` — runs in the renderer Lambda
461
461
  * with access to the data source.
462
462
  *
463
- * @param params - The route parameters, e.g. { id: '42' } for /product/:id
463
+ * @param params - All route parameters, e.g. { lang: 'en', id: '42' } for /:lang/products/:id
464
464
  * @returns Metadata for the page, or null if the page doesn't exist
465
465
  */
466
466
  export type QlaraMetaDataGenerator = (params: Record<string, string>) => Promise<QlaraMetadata | null>;
467
467
 
468
- /** A single route definition with its pattern and metadata generator. */
468
+ /**
469
+ * Optional validation function for a dynamic route.
470
+ * Called before reading the fallback or generating metadata.
471
+ * Use this to cheaply reject invalid param combinations (e.g., unsupported languages).
472
+ *
473
+ * Receives ALL route parameters — you can validate every dynamic segment.
474
+ *
475
+ * @param params - All route parameters, e.g. { lang: 'en', id: '42' }
476
+ * @returns true if the params are valid, false to return 404 immediately
477
+ */
478
+ export type QlaraValidate = (params: Record<string, string>) => Promise<boolean>;
479
+
480
+ /**
481
+ * A single route definition at the leaf page level.
482
+ *
483
+ * Follows the Next.js "generate from bottom up" pattern:
484
+ * define one route entry per leaf page with ALL dynamic params.
485
+ */
469
486
  export interface QlaraRouteDefinition {
470
- /** Dynamic route pattern, e.g. '/product/:id' */
487
+ /** Dynamic route pattern, e.g. '/:lang/products/:id' */
471
488
  route: string;
472
- /** Function that fetches metadata for this route from the data source */
473
- metaDataGenerator: QlaraMetaDataGenerator;
489
+ /**
490
+ * Optional validation function — called before rendering.
491
+ * Return `false` to 404 immediately without generating the page.
492
+ * Receives all params so you can validate every dynamic segment.
493
+ */
494
+ validate?: QlaraValidate;
495
+ /**
496
+ * Function that generates metadata for this route.
497
+ * Preferred name — use this instead of `metaDataGenerator`.
498
+ */
499
+ generateMetadata?: QlaraMetaDataGenerator;
500
+ /**
501
+ * @deprecated Use `generateMetadata` instead. Kept for backward compatibility.
502
+ */
503
+ metaDataGenerator?: QlaraMetaDataGenerator;
474
504
  }
475
505
 
476
506
  /**
477
507
  * The route file default export type: an array of route definitions.
478
508
  *
479
- * Example:
509
+ * Example (single param):
480
510
  * ```typescript
481
511
  * import type { QlaraRoutes } from 'qlara';
482
512
  * const routes: QlaraRoutes = [
483
513
  * {
484
514
  * route: '/product/:id',
485
- * metaDataGenerator: async (params) => {
515
+ * generateMetadata: async (params) => {
486
516
  * const product = await getProduct(params.id);
487
517
  * if (!product) return null;
488
518
  * return { title: product.name, description: product.description };
@@ -491,6 +521,25 @@ export interface QlaraRouteDefinition {
491
521
  * ];
492
522
  * export default routes;
493
523
  * ```
524
+ *
525
+ * Example (multiple params with validation):
526
+ * ```typescript
527
+ * const routes: QlaraRoutes = [
528
+ * {
529
+ * route: '/:lang/products/:id',
530
+ * validate: async (params) => {
531
+ * if (!['en', 'da', 'de'].includes(params.lang)) return false;
532
+ * const product = await getProduct(params.id);
533
+ * return !!product;
534
+ * },
535
+ * generateMetadata: async (params) => {
536
+ * const product = await getProduct(params.id);
537
+ * if (!product) return null;
538
+ * return { title: `${product.name} | Store` };
539
+ * },
540
+ * },
541
+ * ];
542
+ * ```
494
543
  */
495
544
  export type QlaraRoutes = QlaraRouteDefinition[];
496
545