@opentf/web 0.5.0 → 0.6.0

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/server/render.js CHANGED
@@ -14,22 +14,28 @@ import {
14
14
  routes,
15
15
  setRouteState,
16
16
  } from "../runtime/router.js";
17
+ import { resolveMetadata } from "./head.js";
17
18
 
18
19
  /**
19
- * Render `pathname` to an HTML string (the markup for inside `#app`). Falls back
20
- * to the registered `404` page for an unmatched path; returns `null` if there's
21
- * no match and no 404.
20
+ * Render `pathname` to `{ html, metadata, status }`: the markup for inside `#app`,
21
+ * the resolved SEO metadata (for the `<head>`), and an HTTP `status` (200 when the
22
+ * path matched a real route, 404 when it fell back to the registered 404 page — the
23
+ * SSR server uses it; SSG ignores it). `params` (from `getStaticPaths`) override the
24
+ * matched route's params for dynamic routes. Returns `null` if there's no match and
25
+ * no 404 page.
22
26
  */
23
- export async function renderToString(pathname, search = "") {
27
+ export async function renderRoute(pathname, params = null, search = "") {
28
+ const real = matchRoute(pathname);
24
29
  const match =
25
- matchRoute(pathname) ||
26
- (routes.notFound ? { entry: routes.notFound, params: {}, route: null } : null);
30
+ real || (routes.notFound ? { entry: routes.notFound, params: {}, route: null } : null);
27
31
  if (!match) return null;
32
+ if (params) match.params = params;
28
33
 
29
34
  // Let a page reading `router.params`/`pathname`/`query` resolve to this route.
30
35
  setRouteState({ pathname, search, params: match.params });
31
36
 
32
- const props = { params: match.params, query: Object.fromEntries(new URLSearchParams(search)) };
37
+ const query = Object.fromEntries(new URLSearchParams(search));
38
+ const props = { params: match.params, query };
33
39
  let html = (await resolveFactory(match.entry))(props);
34
40
 
35
41
  // Wrap with layouts, most-specific inward to root outermost.
@@ -38,7 +44,20 @@ export async function renderToString(pathname, search = "") {
38
44
  const layout = await resolveFactory(chain[i]);
39
45
  html = layout({ ...props, children: html });
40
46
  }
41
- return html;
47
+
48
+ const metadata = await resolveMetadata({
49
+ route: match.route,
50
+ entry: match.entry,
51
+ params: match.params,
52
+ query,
53
+ });
54
+ return { html, metadata, status: real ? 200 : 404 };
55
+ }
56
+
57
+ /** Back-compat / convenience: render just the `#app` markup for `pathname`. */
58
+ export async function renderToString(pathname, search = "") {
59
+ const result = await renderRoute(pathname, null, search);
60
+ return result ? result.html : null;
42
61
  }
43
62
 
44
63
  /** Substitute `[param]` / `[...rest]` in a route with concrete values. */
@@ -49,16 +68,18 @@ function fillRoute(route, params) {
49
68
  }
50
69
 
51
70
  /**
52
- * Enumerate the concrete paths to pre-render. Static routes are taken as-is;
53
- * dynamic routes (`[param]`) are expanded via the page module's optional
54
- * `getStaticPaths()` (returning `[{ params }]`), else collected as `skipped`.
71
+ * Enumerate the concrete paths to pre-render as `{ path, params }`. Static routes
72
+ * are taken as-is (`params: {}`); dynamic routes (`[param]`) are expanded via the
73
+ * page module's optional `getStaticPaths()` (returning `[{ params }]`), carrying the
74
+ * params forward so the renderer/`generateMetadata` see them. Dynamic routes without
75
+ * `getStaticPaths` are collected as `skipped`.
55
76
  */
56
77
  export async function collectRoutePaths() {
57
78
  const paths = [];
58
79
  const skipped = [];
59
80
  for (const route in routes.pages) {
60
81
  if (!route.includes("[")) {
61
- paths.push(route);
82
+ paths.push({ path: route, params: {} });
62
83
  continue;
63
84
  }
64
85
  const ns = routes.pages[route];
@@ -66,7 +87,8 @@ export async function collectRoutePaths() {
66
87
  ns && (ns.getStaticPaths || (ns.default && ns.default.getStaticPaths));
67
88
  if (typeof getStaticPaths === "function") {
68
89
  for (const entry of (await getStaticPaths()) || []) {
69
- paths.push(fillRoute(route, entry.params || entry));
90
+ const params = entry.params || entry;
91
+ paths.push({ path: fillRoute(route, params), params });
70
92
  }
71
93
  } else {
72
94
  skipped.push(route);
@@ -103,7 +103,10 @@ export function ssgComponent(tag, props, children) {
103
103
  } catch {
104
104
  inner = ""; // fail soft (client renders/handles it)
105
105
  }
106
- return `<${tag}>${inner}</${tag}>`;
106
+ // Stamp the stable styling hook on the host (mirrors CSR's `classList.add`) so
107
+ // tag-hashed components are still styleable by a readable class name.
108
+ const cls = render && render.hostClass;
109
+ return cls ? `<${tag} class="${cls}">${inner}</${tag}>` : `<${tag}>${inner}</${tag}>`;
107
110
  }
108
111
 
109
112
  export { VOID };