@timber-js/app 0.1.24 → 0.1.26
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/adapters/nitro.d.ts +9 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +175 -7
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-dev.d.ts +29 -0
- package/dist/client/browser-dev.d.ts.map +1 -0
- package/dist/client/browser-links.d.ts +32 -0
- package/dist/client/browser-links.d.ts.map +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +46 -20
- package/dist/client/index.js.map +1 -1
- package/dist/client/navigation-context.d.ts +10 -8
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +54 -0
- package/dist/client/transition-root.d.ts.map +1 -0
- package/dist/client/use-router.d.ts +14 -0
- package/dist/client/use-router.d.ts.map +1 -1
- package/dist/server/index.js +264 -218
- package/dist/server/index.js.map +1 -1
- package/dist/server/metadata-platform.d.ts +34 -0
- package/dist/server/metadata-platform.d.ts.map +1 -0
- package/dist/server/metadata-render.d.ts.map +1 -1
- package/dist/server/metadata-social.d.ts +24 -0
- package/dist/server/metadata-social.d.ts.map +1 -0
- package/dist/server/pipeline-interception.d.ts +32 -0
- package/dist/server/pipeline-interception.d.ts.map +1 -0
- package/dist/server/pipeline-metadata.d.ts +31 -0
- package/dist/server/pipeline-metadata.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/nitro.ts +187 -10
- package/src/cli.ts +10 -3
- package/src/client/browser-dev.ts +142 -0
- package/src/client/browser-entry.ts +32 -222
- package/src/client/browser-links.ts +90 -0
- package/src/client/index.ts +1 -1
- package/src/client/navigation-context.ts +39 -9
- package/src/client/transition-root.tsx +86 -0
- package/src/client/use-router.ts +17 -15
- package/src/server/metadata-platform.ts +229 -0
- package/src/server/metadata-render.ts +9 -363
- package/src/server/metadata-social.ts +184 -0
- package/src/server/pipeline-interception.ts +76 -0
- package/src/server/pipeline-metadata.ts +90 -0
- package/src/server/pipeline.ts +2 -148
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-router.d.ts","sourceRoot":"","sources":["../../src/client/use-router.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"use-router.d.ts","sourceRoot":"","sources":["../../src/client/use-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,6DAA6D;IAC7D,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5D,sDAAsD;IACtD,OAAO,IAAI,IAAI,CAAC;IAChB,+BAA+B;IAC/B,IAAI,IAAI,IAAI,CAAC;IACb,kCAAkC;IAClC,OAAO,IAAI,IAAI,CAAC;IAChB,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,IAAI,iBAAiB,CA4C7C"}
|
package/dist/server/index.js
CHANGED
|
@@ -662,6 +662,110 @@ function hasOnRequestError() {
|
|
|
662
662
|
return _onRequestError !== null;
|
|
663
663
|
}
|
|
664
664
|
//#endregion
|
|
665
|
+
//#region src/server/pipeline-metadata.ts
|
|
666
|
+
/**
|
|
667
|
+
* Metadata route helpers for the request pipeline.
|
|
668
|
+
*
|
|
669
|
+
* Handles serving static metadata files and serializing sitemap responses.
|
|
670
|
+
* Extracted from pipeline.ts to keep files under 500 lines.
|
|
671
|
+
*
|
|
672
|
+
* See design/16-metadata.md §"Metadata Routes"
|
|
673
|
+
*/
|
|
674
|
+
/**
|
|
675
|
+
* Content types that are text-based and should include charset=utf-8.
|
|
676
|
+
* Binary formats (images) should not include charset.
|
|
677
|
+
*/
|
|
678
|
+
var TEXT_CONTENT_TYPES = new Set([
|
|
679
|
+
"application/xml",
|
|
680
|
+
"text/plain",
|
|
681
|
+
"application/json",
|
|
682
|
+
"application/manifest+json",
|
|
683
|
+
"image/svg+xml"
|
|
684
|
+
]);
|
|
685
|
+
/**
|
|
686
|
+
* Serve a static metadata file by reading it from disk.
|
|
687
|
+
*
|
|
688
|
+
* Static metadata route files (.xml, .txt, .json, .png, .ico, .svg, etc.)
|
|
689
|
+
* are served as-is with the appropriate Content-Type header.
|
|
690
|
+
* Text files include charset=utf-8; binary files do not.
|
|
691
|
+
*
|
|
692
|
+
* See design/16-metadata.md §"Metadata Routes"
|
|
693
|
+
*/
|
|
694
|
+
async function serveStaticMetadataFile(metaMatch) {
|
|
695
|
+
const { contentType, file } = metaMatch;
|
|
696
|
+
const isText = TEXT_CONTENT_TYPES.has(contentType);
|
|
697
|
+
const body = await readFile(file.filePath);
|
|
698
|
+
const headers = {
|
|
699
|
+
"Content-Type": isText ? `${contentType}; charset=utf-8` : contentType,
|
|
700
|
+
"Content-Length": String(body.byteLength)
|
|
701
|
+
};
|
|
702
|
+
return new Response(body, {
|
|
703
|
+
status: 200,
|
|
704
|
+
headers
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Serialize a sitemap array to XML.
|
|
709
|
+
* Follows the sitemap.org protocol: https://www.sitemaps.org/protocol.html
|
|
710
|
+
*/
|
|
711
|
+
function serializeSitemap(entries) {
|
|
712
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map((e) => {
|
|
713
|
+
let xml = ` <url>\n <loc>${escapeXml(e.url)}</loc>`;
|
|
714
|
+
if (e.lastModified) {
|
|
715
|
+
const date = e.lastModified instanceof Date ? e.lastModified.toISOString() : e.lastModified;
|
|
716
|
+
xml += `\n <lastmod>${escapeXml(date)}</lastmod>`;
|
|
717
|
+
}
|
|
718
|
+
if (e.changeFrequency) xml += `\n <changefreq>${escapeXml(e.changeFrequency)}</changefreq>`;
|
|
719
|
+
if (e.priority !== void 0) xml += `\n <priority>${e.priority}</priority>`;
|
|
720
|
+
xml += "\n </url>";
|
|
721
|
+
return xml;
|
|
722
|
+
}).join("\n")}\n</urlset>`;
|
|
723
|
+
}
|
|
724
|
+
/** Escape special XML characters. */
|
|
725
|
+
function escapeXml(str) {
|
|
726
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/server/pipeline-interception.ts
|
|
730
|
+
/**
|
|
731
|
+
* Check if an intercepting route applies for this soft navigation.
|
|
732
|
+
*
|
|
733
|
+
* Matches the target pathname against interception rewrites, constrained
|
|
734
|
+
* by the source URL (X-Timber-URL header — where the user navigates FROM).
|
|
735
|
+
*
|
|
736
|
+
* Returns the source pathname to re-match if interception applies, or null.
|
|
737
|
+
*/
|
|
738
|
+
function findInterceptionMatch(targetPathname, sourceUrl, rewrites) {
|
|
739
|
+
for (const rewrite of rewrites) {
|
|
740
|
+
if (!sourceUrl.startsWith(rewrite.interceptingPrefix)) continue;
|
|
741
|
+
if (pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)) return { sourcePathname: rewrite.interceptingPrefix };
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Check if a pathname matches a URL pattern with dynamic segments.
|
|
747
|
+
*
|
|
748
|
+
* Supports [param] (single segment) and [...param] (one or more segments).
|
|
749
|
+
* Static segments must match exactly.
|
|
750
|
+
*/
|
|
751
|
+
function pathnameMatchesPattern(pathname, pattern) {
|
|
752
|
+
const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
753
|
+
const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
|
|
754
|
+
let pi = 0;
|
|
755
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
756
|
+
const segment = patternParts[i];
|
|
757
|
+
if (segment.startsWith("[...") || segment.startsWith("[[...")) return pi < pathParts.length || segment.startsWith("[[...");
|
|
758
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
759
|
+
if (pi >= pathParts.length) return false;
|
|
760
|
+
pi++;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (pi >= pathParts.length || pathParts[pi] !== segment) return false;
|
|
764
|
+
pi++;
|
|
765
|
+
}
|
|
766
|
+
return pi === pathParts.length;
|
|
767
|
+
}
|
|
768
|
+
//#endregion
|
|
665
769
|
//#region src/server/pipeline.ts
|
|
666
770
|
/**
|
|
667
771
|
* Request pipeline — the central dispatch for all timber.js requests.
|
|
@@ -909,44 +1013,6 @@ async function fireOnRequestError(error, req, phase) {
|
|
|
909
1013
|
});
|
|
910
1014
|
}
|
|
911
1015
|
/**
|
|
912
|
-
* Check if an intercepting route applies for this soft navigation.
|
|
913
|
-
*
|
|
914
|
-
* Matches the target pathname against interception rewrites, constrained
|
|
915
|
-
* by the source URL (X-Timber-URL header — where the user navigates FROM).
|
|
916
|
-
*
|
|
917
|
-
* Returns the source pathname to re-match if interception applies, or null.
|
|
918
|
-
*/
|
|
919
|
-
function findInterceptionMatch(targetPathname, sourceUrl, rewrites) {
|
|
920
|
-
for (const rewrite of rewrites) {
|
|
921
|
-
if (!sourceUrl.startsWith(rewrite.interceptingPrefix)) continue;
|
|
922
|
-
if (pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)) return { sourcePathname: rewrite.interceptingPrefix };
|
|
923
|
-
}
|
|
924
|
-
return null;
|
|
925
|
-
}
|
|
926
|
-
/**
|
|
927
|
-
* Check if a pathname matches a URL pattern with dynamic segments.
|
|
928
|
-
*
|
|
929
|
-
* Supports [param] (single segment) and [...param] (one or more segments).
|
|
930
|
-
* Static segments must match exactly.
|
|
931
|
-
*/
|
|
932
|
-
function pathnameMatchesPattern(pathname, pattern) {
|
|
933
|
-
const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
934
|
-
const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
|
|
935
|
-
let pi = 0;
|
|
936
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
937
|
-
const segment = patternParts[i];
|
|
938
|
-
if (segment.startsWith("[...") || segment.startsWith("[[...")) return pi < pathParts.length || segment.startsWith("[[...");
|
|
939
|
-
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
940
|
-
if (pi >= pathParts.length) return false;
|
|
941
|
-
pi++;
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
if (pi >= pathParts.length || pathParts[pi] !== segment) return false;
|
|
945
|
-
pi++;
|
|
946
|
-
}
|
|
947
|
-
return pi === pathParts.length;
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
1016
|
* Apply all Set-Cookie headers from the cookie jar to a Headers object.
|
|
951
1017
|
* Each cookie gets its own Set-Cookie header per RFC 6265 §4.1.
|
|
952
1018
|
*/
|
|
@@ -976,60 +1042,6 @@ function ensureMutableResponse(response) {
|
|
|
976
1042
|
});
|
|
977
1043
|
}
|
|
978
1044
|
}
|
|
979
|
-
/**
|
|
980
|
-
* Serialize a sitemap array to XML.
|
|
981
|
-
* Follows the sitemap.org protocol: https://www.sitemaps.org/protocol.html
|
|
982
|
-
*/
|
|
983
|
-
function serializeSitemap(entries) {
|
|
984
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map((e) => {
|
|
985
|
-
let xml = ` <url>\n <loc>${escapeXml(e.url)}</loc>`;
|
|
986
|
-
if (e.lastModified) {
|
|
987
|
-
const date = e.lastModified instanceof Date ? e.lastModified.toISOString() : e.lastModified;
|
|
988
|
-
xml += `\n <lastmod>${escapeXml(date)}</lastmod>`;
|
|
989
|
-
}
|
|
990
|
-
if (e.changeFrequency) xml += `\n <changefreq>${escapeXml(e.changeFrequency)}</changefreq>`;
|
|
991
|
-
if (e.priority !== void 0) xml += `\n <priority>${e.priority}</priority>`;
|
|
992
|
-
xml += "\n </url>";
|
|
993
|
-
return xml;
|
|
994
|
-
}).join("\n")}\n</urlset>`;
|
|
995
|
-
}
|
|
996
|
-
/** Escape special XML characters. */
|
|
997
|
-
function escapeXml(str) {
|
|
998
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Content types that are text-based and should include charset=utf-8.
|
|
1002
|
-
* Binary formats (images) should not include charset.
|
|
1003
|
-
*/
|
|
1004
|
-
var TEXT_CONTENT_TYPES = new Set([
|
|
1005
|
-
"application/xml",
|
|
1006
|
-
"text/plain",
|
|
1007
|
-
"application/json",
|
|
1008
|
-
"application/manifest+json",
|
|
1009
|
-
"image/svg+xml"
|
|
1010
|
-
]);
|
|
1011
|
-
/**
|
|
1012
|
-
* Serve a static metadata file by reading it from disk.
|
|
1013
|
-
*
|
|
1014
|
-
* Static metadata route files (.xml, .txt, .json, .png, .ico, .svg, etc.)
|
|
1015
|
-
* are served as-is with the appropriate Content-Type header.
|
|
1016
|
-
* Text files include charset=utf-8; binary files do not.
|
|
1017
|
-
*
|
|
1018
|
-
* See design/16-metadata.md §"Metadata Routes"
|
|
1019
|
-
*/
|
|
1020
|
-
async function serveStaticMetadataFile(metaMatch) {
|
|
1021
|
-
const { contentType, file } = metaMatch;
|
|
1022
|
-
const isText = TEXT_CONTENT_TYPES.has(contentType);
|
|
1023
|
-
const body = await readFile(file.filePath);
|
|
1024
|
-
const headers = {
|
|
1025
|
-
"Content-Type": isText ? `${contentType}; charset=utf-8` : contentType,
|
|
1026
|
-
"Content-Length": String(body.byteLength)
|
|
1027
|
-
};
|
|
1028
|
-
return new Response(body, {
|
|
1029
|
-
status: 200,
|
|
1030
|
-
headers
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
1045
|
//#endregion
|
|
1034
1046
|
//#region src/server/build-manifest.ts
|
|
1035
1047
|
/**
|
|
@@ -1831,135 +1843,13 @@ function resolveLimit(kind, config) {
|
|
|
1831
1843
|
return userLimits?.uploadBodySize ? parseBodySize(userLimits.uploadBodySize) : DEFAULT_LIMITS.uploadBodySize;
|
|
1832
1844
|
}
|
|
1833
1845
|
//#endregion
|
|
1834
|
-
//#region src/server/metadata-
|
|
1846
|
+
//#region src/server/metadata-social.ts
|
|
1835
1847
|
/**
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
1838
|
-
* Each descriptor has a `tag` ('title', 'meta', 'link') and either
|
|
1839
|
-
* `content` (for <title>) or `attrs` (for <meta>/<link>).
|
|
1848
|
+
* Render Open Graph metadata into head element descriptors.
|
|
1840
1849
|
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1850
|
+
* Handles og:title, og:description, og:image (with dimensions/alt),
|
|
1851
|
+
* og:video, og:audio, og:article:author, and other OG properties.
|
|
1843
1852
|
*/
|
|
1844
|
-
function renderMetadataToElements(metadata) {
|
|
1845
|
-
const elements = [];
|
|
1846
|
-
if (typeof metadata.title === "string") elements.push({
|
|
1847
|
-
tag: "title",
|
|
1848
|
-
content: metadata.title
|
|
1849
|
-
});
|
|
1850
|
-
const simpleMetaProps = [
|
|
1851
|
-
["description", metadata.description],
|
|
1852
|
-
["generator", metadata.generator],
|
|
1853
|
-
["application-name", metadata.applicationName],
|
|
1854
|
-
["referrer", metadata.referrer],
|
|
1855
|
-
["category", metadata.category],
|
|
1856
|
-
["creator", metadata.creator],
|
|
1857
|
-
["publisher", metadata.publisher]
|
|
1858
|
-
];
|
|
1859
|
-
for (const [name, content] of simpleMetaProps) if (content) elements.push({
|
|
1860
|
-
tag: "meta",
|
|
1861
|
-
attrs: {
|
|
1862
|
-
name,
|
|
1863
|
-
content
|
|
1864
|
-
}
|
|
1865
|
-
});
|
|
1866
|
-
if (metadata.keywords) {
|
|
1867
|
-
const content = Array.isArray(metadata.keywords) ? metadata.keywords.join(", ") : metadata.keywords;
|
|
1868
|
-
elements.push({
|
|
1869
|
-
tag: "meta",
|
|
1870
|
-
attrs: {
|
|
1871
|
-
name: "keywords",
|
|
1872
|
-
content
|
|
1873
|
-
}
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1876
|
-
if (metadata.robots) {
|
|
1877
|
-
const content = typeof metadata.robots === "string" ? metadata.robots : renderRobotsObject(metadata.robots);
|
|
1878
|
-
elements.push({
|
|
1879
|
-
tag: "meta",
|
|
1880
|
-
attrs: {
|
|
1881
|
-
name: "robots",
|
|
1882
|
-
content
|
|
1883
|
-
}
|
|
1884
|
-
});
|
|
1885
|
-
if (typeof metadata.robots === "object" && metadata.robots.googleBot) {
|
|
1886
|
-
const gbContent = typeof metadata.robots.googleBot === "string" ? metadata.robots.googleBot : renderRobotsObject(metadata.robots.googleBot);
|
|
1887
|
-
elements.push({
|
|
1888
|
-
tag: "meta",
|
|
1889
|
-
attrs: {
|
|
1890
|
-
name: "googlebot",
|
|
1891
|
-
content: gbContent
|
|
1892
|
-
}
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
if (metadata.openGraph) renderOpenGraph(metadata.openGraph, elements);
|
|
1897
|
-
if (metadata.twitter) renderTwitter(metadata.twitter, elements);
|
|
1898
|
-
if (metadata.icons) renderIcons(metadata.icons, elements);
|
|
1899
|
-
if (metadata.manifest) elements.push({
|
|
1900
|
-
tag: "link",
|
|
1901
|
-
attrs: {
|
|
1902
|
-
rel: "manifest",
|
|
1903
|
-
href: metadata.manifest
|
|
1904
|
-
}
|
|
1905
|
-
});
|
|
1906
|
-
if (metadata.alternates) renderAlternates(metadata.alternates, elements);
|
|
1907
|
-
if (metadata.verification) renderVerification(metadata.verification, elements);
|
|
1908
|
-
if (metadata.formatDetection) {
|
|
1909
|
-
const parts = [];
|
|
1910
|
-
if (metadata.formatDetection.telephone === false) parts.push("telephone=no");
|
|
1911
|
-
if (metadata.formatDetection.email === false) parts.push("email=no");
|
|
1912
|
-
if (metadata.formatDetection.address === false) parts.push("address=no");
|
|
1913
|
-
if (parts.length > 0) elements.push({
|
|
1914
|
-
tag: "meta",
|
|
1915
|
-
attrs: {
|
|
1916
|
-
name: "format-detection",
|
|
1917
|
-
content: parts.join(", ")
|
|
1918
|
-
}
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
if (metadata.authors) {
|
|
1922
|
-
const authorList = Array.isArray(metadata.authors) ? metadata.authors : [metadata.authors];
|
|
1923
|
-
for (const author of authorList) {
|
|
1924
|
-
if (author.name) elements.push({
|
|
1925
|
-
tag: "meta",
|
|
1926
|
-
attrs: {
|
|
1927
|
-
name: "author",
|
|
1928
|
-
content: author.name
|
|
1929
|
-
}
|
|
1930
|
-
});
|
|
1931
|
-
if (author.url) elements.push({
|
|
1932
|
-
tag: "link",
|
|
1933
|
-
attrs: {
|
|
1934
|
-
rel: "author",
|
|
1935
|
-
href: author.url
|
|
1936
|
-
}
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
if (metadata.appleWebApp) renderAppleWebApp(metadata.appleWebApp, elements);
|
|
1941
|
-
if (metadata.appLinks) renderAppLinks(metadata.appLinks, elements);
|
|
1942
|
-
if (metadata.itunes) renderItunes(metadata.itunes, elements);
|
|
1943
|
-
if (metadata.other) for (const [name, value] of Object.entries(metadata.other)) {
|
|
1944
|
-
const content = Array.isArray(value) ? value.join(", ") : value;
|
|
1945
|
-
elements.push({
|
|
1946
|
-
tag: "meta",
|
|
1947
|
-
attrs: {
|
|
1948
|
-
name,
|
|
1949
|
-
content
|
|
1950
|
-
}
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
return elements;
|
|
1954
|
-
}
|
|
1955
|
-
function renderRobotsObject(robots) {
|
|
1956
|
-
const parts = [];
|
|
1957
|
-
if (robots.index === true) parts.push("index");
|
|
1958
|
-
if (robots.index === false) parts.push("noindex");
|
|
1959
|
-
if (robots.follow === true) parts.push("follow");
|
|
1960
|
-
if (robots.follow === false) parts.push("nofollow");
|
|
1961
|
-
return parts.join(", ");
|
|
1962
|
-
}
|
|
1963
1853
|
function renderOpenGraph(og, elements) {
|
|
1964
1854
|
const simpleProps = [
|
|
1965
1855
|
["og:title", og.title],
|
|
@@ -2040,6 +1930,12 @@ function renderOpenGraph(og, elements) {
|
|
|
2040
1930
|
}
|
|
2041
1931
|
});
|
|
2042
1932
|
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Render Twitter Card metadata into head element descriptors.
|
|
1935
|
+
*
|
|
1936
|
+
* Handles twitter:card, twitter:site, twitter:title, twitter:image,
|
|
1937
|
+
* twitter:player, and twitter:app (per-platform name/id/url).
|
|
1938
|
+
*/
|
|
2043
1939
|
function renderTwitter(tw, elements) {
|
|
2044
1940
|
const simpleProps = [
|
|
2045
1941
|
["twitter:card", tw.card],
|
|
@@ -2144,6 +2040,11 @@ function renderTwitter(tw, elements) {
|
|
|
2144
2040
|
}
|
|
2145
2041
|
}
|
|
2146
2042
|
}
|
|
2043
|
+
//#endregion
|
|
2044
|
+
//#region src/server/metadata-platform.ts
|
|
2045
|
+
/**
|
|
2046
|
+
* Render icon link elements (favicon, shortcut, apple-touch-icon, custom).
|
|
2047
|
+
*/
|
|
2147
2048
|
function renderIcons(icons, elements) {
|
|
2148
2049
|
if (icons.icon) {
|
|
2149
2050
|
if (typeof icons.icon === "string") elements.push({
|
|
@@ -2209,6 +2110,9 @@ function renderIcons(icons, elements) {
|
|
|
2209
2110
|
});
|
|
2210
2111
|
}
|
|
2211
2112
|
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Render alternate link elements (canonical, hreflang, media, types).
|
|
2115
|
+
*/
|
|
2212
2116
|
function renderAlternates(alternates, elements) {
|
|
2213
2117
|
if (alternates.canonical) elements.push({
|
|
2214
2118
|
tag: "link",
|
|
@@ -2242,6 +2146,9 @@ function renderAlternates(alternates, elements) {
|
|
|
2242
2146
|
}
|
|
2243
2147
|
});
|
|
2244
2148
|
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Render site verification meta tags (Google, Yahoo, Yandex, custom).
|
|
2151
|
+
*/
|
|
2245
2152
|
function renderVerification(verification, elements) {
|
|
2246
2153
|
const verificationProps = [
|
|
2247
2154
|
["google-site-verification", verification.google],
|
|
@@ -2266,6 +2173,9 @@ function renderVerification(verification, elements) {
|
|
|
2266
2173
|
});
|
|
2267
2174
|
}
|
|
2268
2175
|
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Render Apple Web App meta tags and startup image links.
|
|
2178
|
+
*/
|
|
2269
2179
|
function renderAppleWebApp(appleWebApp, elements) {
|
|
2270
2180
|
if (appleWebApp.capable) elements.push({
|
|
2271
2181
|
tag: "meta",
|
|
@@ -2303,6 +2213,9 @@ function renderAppleWebApp(appleWebApp, elements) {
|
|
|
2303
2213
|
}
|
|
2304
2214
|
}
|
|
2305
2215
|
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Render App Links (al:*) meta tags for deep linking across platforms.
|
|
2218
|
+
*/
|
|
2306
2219
|
function renderAppLinks(appLinks, elements) {
|
|
2307
2220
|
const platformEntries = [
|
|
2308
2221
|
["ios", appLinks.ios],
|
|
@@ -2338,6 +2251,9 @@ function renderAppLinks(appLinks, elements) {
|
|
|
2338
2251
|
});
|
|
2339
2252
|
}
|
|
2340
2253
|
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Render Apple iTunes smart banner meta tag.
|
|
2256
|
+
*/
|
|
2341
2257
|
function renderItunes(itunes, elements) {
|
|
2342
2258
|
const parts = [`app-id=${itunes.appId}`];
|
|
2343
2259
|
if (itunes.affiliateData) parts.push(`affiliate-data=${itunes.affiliateData}`);
|
|
@@ -2351,6 +2267,136 @@ function renderItunes(itunes, elements) {
|
|
|
2351
2267
|
});
|
|
2352
2268
|
}
|
|
2353
2269
|
//#endregion
|
|
2270
|
+
//#region src/server/metadata-render.ts
|
|
2271
|
+
/**
|
|
2272
|
+
* Convert resolved metadata into an array of head element descriptors.
|
|
2273
|
+
*
|
|
2274
|
+
* Each descriptor has a `tag` ('title', 'meta', 'link') and either
|
|
2275
|
+
* `content` (for <title>) or `attrs` (for <meta>/<link>).
|
|
2276
|
+
*
|
|
2277
|
+
* The framework's MetadataResolver component consumes these descriptors
|
|
2278
|
+
* and renders them into the <head>.
|
|
2279
|
+
*/
|
|
2280
|
+
function renderMetadataToElements(metadata) {
|
|
2281
|
+
const elements = [];
|
|
2282
|
+
if (typeof metadata.title === "string") elements.push({
|
|
2283
|
+
tag: "title",
|
|
2284
|
+
content: metadata.title
|
|
2285
|
+
});
|
|
2286
|
+
const simpleMetaProps = [
|
|
2287
|
+
["description", metadata.description],
|
|
2288
|
+
["generator", metadata.generator],
|
|
2289
|
+
["application-name", metadata.applicationName],
|
|
2290
|
+
["referrer", metadata.referrer],
|
|
2291
|
+
["category", metadata.category],
|
|
2292
|
+
["creator", metadata.creator],
|
|
2293
|
+
["publisher", metadata.publisher]
|
|
2294
|
+
];
|
|
2295
|
+
for (const [name, content] of simpleMetaProps) if (content) elements.push({
|
|
2296
|
+
tag: "meta",
|
|
2297
|
+
attrs: {
|
|
2298
|
+
name,
|
|
2299
|
+
content
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
if (metadata.keywords) {
|
|
2303
|
+
const content = Array.isArray(metadata.keywords) ? metadata.keywords.join(", ") : metadata.keywords;
|
|
2304
|
+
elements.push({
|
|
2305
|
+
tag: "meta",
|
|
2306
|
+
attrs: {
|
|
2307
|
+
name: "keywords",
|
|
2308
|
+
content
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
if (metadata.robots) {
|
|
2313
|
+
const content = typeof metadata.robots === "string" ? metadata.robots : renderRobotsObject(metadata.robots);
|
|
2314
|
+
elements.push({
|
|
2315
|
+
tag: "meta",
|
|
2316
|
+
attrs: {
|
|
2317
|
+
name: "robots",
|
|
2318
|
+
content
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
if (typeof metadata.robots === "object" && metadata.robots.googleBot) {
|
|
2322
|
+
const gbContent = typeof metadata.robots.googleBot === "string" ? metadata.robots.googleBot : renderRobotsObject(metadata.robots.googleBot);
|
|
2323
|
+
elements.push({
|
|
2324
|
+
tag: "meta",
|
|
2325
|
+
attrs: {
|
|
2326
|
+
name: "googlebot",
|
|
2327
|
+
content: gbContent
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
if (metadata.openGraph) renderOpenGraph(metadata.openGraph, elements);
|
|
2333
|
+
if (metadata.twitter) renderTwitter(metadata.twitter, elements);
|
|
2334
|
+
if (metadata.icons) renderIcons(metadata.icons, elements);
|
|
2335
|
+
if (metadata.manifest) elements.push({
|
|
2336
|
+
tag: "link",
|
|
2337
|
+
attrs: {
|
|
2338
|
+
rel: "manifest",
|
|
2339
|
+
href: metadata.manifest
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
if (metadata.alternates) renderAlternates(metadata.alternates, elements);
|
|
2343
|
+
if (metadata.verification) renderVerification(metadata.verification, elements);
|
|
2344
|
+
if (metadata.formatDetection) {
|
|
2345
|
+
const parts = [];
|
|
2346
|
+
if (metadata.formatDetection.telephone === false) parts.push("telephone=no");
|
|
2347
|
+
if (metadata.formatDetection.email === false) parts.push("email=no");
|
|
2348
|
+
if (metadata.formatDetection.address === false) parts.push("address=no");
|
|
2349
|
+
if (parts.length > 0) elements.push({
|
|
2350
|
+
tag: "meta",
|
|
2351
|
+
attrs: {
|
|
2352
|
+
name: "format-detection",
|
|
2353
|
+
content: parts.join(", ")
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
if (metadata.authors) {
|
|
2358
|
+
const authorList = Array.isArray(metadata.authors) ? metadata.authors : [metadata.authors];
|
|
2359
|
+
for (const author of authorList) {
|
|
2360
|
+
if (author.name) elements.push({
|
|
2361
|
+
tag: "meta",
|
|
2362
|
+
attrs: {
|
|
2363
|
+
name: "author",
|
|
2364
|
+
content: author.name
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
if (author.url) elements.push({
|
|
2368
|
+
tag: "link",
|
|
2369
|
+
attrs: {
|
|
2370
|
+
rel: "author",
|
|
2371
|
+
href: author.url
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (metadata.appleWebApp) renderAppleWebApp(metadata.appleWebApp, elements);
|
|
2377
|
+
if (metadata.appLinks) renderAppLinks(metadata.appLinks, elements);
|
|
2378
|
+
if (metadata.itunes) renderItunes(metadata.itunes, elements);
|
|
2379
|
+
if (metadata.other) for (const [name, value] of Object.entries(metadata.other)) {
|
|
2380
|
+
const content = Array.isArray(value) ? value.join(", ") : value;
|
|
2381
|
+
elements.push({
|
|
2382
|
+
tag: "meta",
|
|
2383
|
+
attrs: {
|
|
2384
|
+
name,
|
|
2385
|
+
content
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
return elements;
|
|
2390
|
+
}
|
|
2391
|
+
function renderRobotsObject(robots) {
|
|
2392
|
+
const parts = [];
|
|
2393
|
+
if (robots.index === true) parts.push("index");
|
|
2394
|
+
if (robots.index === false) parts.push("noindex");
|
|
2395
|
+
if (robots.follow === true) parts.push("follow");
|
|
2396
|
+
if (robots.follow === false) parts.push("nofollow");
|
|
2397
|
+
return parts.join(", ");
|
|
2398
|
+
}
|
|
2399
|
+
//#endregion
|
|
2354
2400
|
//#region src/server/metadata.ts
|
|
2355
2401
|
/**
|
|
2356
2402
|
* Resolve a title value with an optional template.
|