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 +24 -12
- package/dist/aws.d.cts +1 -1
- package/dist/aws.d.ts +1 -1
- package/dist/aws.js +24 -12
- package/dist/cli.js +24 -12
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/plugin/next.d.cts +1 -1
- package/dist/plugin/next.d.ts +1 -1
- package/dist/{types--KPPgCtc.d.cts → types-BmSR1R_Q.d.cts} +57 -9
- package/dist/{types--KPPgCtc.d.ts → types-BmSR1R_Q.d.ts} +57 -9
- package/package.json +1 -1
- package/src/provider/aws/renderer.ts +53 -34
- package/src/types.ts +57 -8
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
|
-
|
|
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}\\":\\"${
|
|
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}\\",\\"${
|
|
637
|
+
`[\\"${param}\\",\\"${paramPlaceholder(param)}\\",\\"d\\"]`
|
|
636
638
|
);
|
|
637
639
|
}
|
|
638
|
-
const
|
|
639
|
-
if (
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
package/dist/aws.d.ts
CHANGED
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
|
-
|
|
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}\\":\\"${
|
|
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}\\",\\"${
|
|
633
|
+
`[\\"${param}\\",\\"${paramPlaceholder(param)}\\",\\"d\\"]`
|
|
632
634
|
);
|
|
633
635
|
}
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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}\\":\\"${
|
|
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}\\",\\"${
|
|
641
|
+
`[\\"${param}\\",\\"${paramPlaceholder(param)}\\",\\"d\\"]`
|
|
640
642
|
);
|
|
641
643
|
}
|
|
642
|
-
const
|
|
643
|
-
if (
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/dist/plugin/next.d.cts
CHANGED
package/dist/plugin/next.d.ts
CHANGED
|
@@ -315,31 +315,60 @@ interface QlaraMetadata {
|
|
|
315
315
|
other?: Record<string, string | number | (string | number)[]>;
|
|
316
316
|
}
|
|
317
317
|
/**
|
|
318
|
-
* Function that
|
|
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 -
|
|
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
|
-
/**
|
|
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. '/
|
|
344
|
+
/** Dynamic route pattern, e.g. '/:lang/products/:id' */
|
|
329
345
|
route: string;
|
|
330
|
-
/**
|
|
331
|
-
|
|
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
|
-
*
|
|
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,
|
|
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
|
|
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 -
|
|
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
|
-
/**
|
|
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. '/
|
|
344
|
+
/** Dynamic route pattern, e.g. '/:lang/products/:id' */
|
|
329
345
|
route: string;
|
|
330
|
-
/**
|
|
331
|
-
|
|
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
|
-
*
|
|
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,
|
|
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
|
@@ -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
|
|
99
|
-
*
|
|
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(
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
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
|
-
*
|
|
110
|
-
*
|
|
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
|
|
113
|
-
|
|
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
|
|
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,
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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,
|
|
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.
|
|
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(
|
|
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
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
new RegExp(
|
|
1154
|
-
|
|
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
|
|
1158
|
-
const
|
|
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 (
|
|
1162
|
-
metadata = await
|
|
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
|
|
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 -
|
|
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
|
-
/**
|
|
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. '/
|
|
487
|
+
/** Dynamic route pattern, e.g. '/:lang/products/:id' */
|
|
471
488
|
route: string;
|
|
472
|
-
/**
|
|
473
|
-
|
|
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
|
-
*
|
|
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
|
|