@timber-js/app 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/adapters/nitro.d.ts.map +1 -1
  2. package/dist/adapters/nitro.js +4 -3
  3. package/dist/adapters/nitro.js.map +1 -1
  4. package/dist/cli.js +1 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client/browser-dev.d.ts +29 -0
  7. package/dist/client/browser-dev.d.ts.map +1 -0
  8. package/dist/client/browser-links.d.ts +32 -0
  9. package/dist/client/browser-links.d.ts.map +1 -0
  10. package/dist/client/index.d.ts +1 -1
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +46 -20
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/navigation-context.d.ts +10 -8
  15. package/dist/client/navigation-context.d.ts.map +1 -1
  16. package/dist/client/transition-root.d.ts +54 -0
  17. package/dist/client/transition-root.d.ts.map +1 -0
  18. package/dist/client/use-router.d.ts +14 -0
  19. package/dist/client/use-router.d.ts.map +1 -1
  20. package/dist/server/index.js +264 -218
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/metadata-platform.d.ts +34 -0
  23. package/dist/server/metadata-platform.d.ts.map +1 -0
  24. package/dist/server/metadata-render.d.ts.map +1 -1
  25. package/dist/server/metadata-social.d.ts +24 -0
  26. package/dist/server/metadata-social.d.ts.map +1 -0
  27. package/dist/server/pipeline-interception.d.ts +32 -0
  28. package/dist/server/pipeline-interception.d.ts.map +1 -0
  29. package/dist/server/pipeline-metadata.d.ts +31 -0
  30. package/dist/server/pipeline-metadata.d.ts.map +1 -0
  31. package/dist/server/pipeline.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/adapters/nitro.ts +9 -7
  34. package/src/cli.ts +9 -2
  35. package/src/client/browser-dev.ts +142 -0
  36. package/src/client/browser-entry.ts +32 -222
  37. package/src/client/browser-links.ts +90 -0
  38. package/src/client/index.ts +1 -1
  39. package/src/client/navigation-context.ts +39 -9
  40. package/src/client/transition-root.tsx +86 -0
  41. package/src/client/use-router.ts +17 -15
  42. package/src/server/metadata-platform.ts +229 -0
  43. package/src/server/metadata-render.ts +9 -363
  44. package/src/server/metadata-social.ts +184 -0
  45. package/src/server/pipeline-interception.ts +76 -0
  46. package/src/server/pipeline-metadata.ts +90 -0
  47. 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;;;;;;;;;GASG;AAKH,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,CAuD7C"}
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"}
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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-render.ts
1846
+ //#region src/server/metadata-social.ts
1835
1847
  /**
1836
- * Convert resolved metadata into an array of head element descriptors.
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
- * The framework's MetadataResolver component consumes these descriptors
1842
- * and renders them into the <head>.
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.