@symbo.ls/brender 3.5.1 → 3.6.3

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/README.md CHANGED
@@ -63,7 +63,7 @@ Browser DOM (static) DOMQL Tree (no nodes) After hydrate()
63
63
  | `hydrate.js` | `collectBrNodes()` — scans DOM for data-br nodes; `hydrate()` — reconnects DOMQL tree to DOM |
64
64
  | `env.js` | `createEnv()` — linkedom virtual DOM with browser API stubs (requestAnimationFrame, history, location, etc.) |
65
65
  | `keys.js` | `resetKeys()`, `assignKeys()` — stamps data-br on DOM nodes; `mapKeysToElements()` — builds registry |
66
- | `metadata.js` | `extractMetadata()`, `generateHeadHtml()` SEO meta tags from page definitions |
66
+ | `metadata.js` | Re-exports from [`@symbo.ls/helmet`](../helmet/) — `extractMetadata()`, `generateHeadHtml()`, `resolveMetadata()`, `applyMetadata()` |
67
67
  | `load.js` | `loadProject()` — imports a symbols/ directory; `loadAndRenderAll()` — renders every route |
68
68
  | `index.js` | Re-exports everything |
69
69
 
@@ -168,13 +168,15 @@ Creates a linkedom virtual DOM environment with stubs for browser APIs that DOMQ
168
168
 
169
169
  ### `generateHeadHtml(metadata)`
170
170
 
171
- Converts a metadata object into HTML head tags:
171
+ Converts a metadata object into HTML head tags. Provided by [`@symbo.ls/helmet`](../helmet/):
172
172
 
173
173
  ```js
174
174
  generateHeadHtml({ title: 'My Page', description: 'About', 'og:image': '/img.png' })
175
175
  // -> '<meta charset="UTF-8">\n<title>My Page</title>\n<meta name="description" content="About">\n<meta property="og:image" content="/img.png">'
176
176
  ```
177
177
 
178
+ Metadata values can also be functions — see the [helmet plugin](../helmet/) for details.
179
+
178
180
  ## Examples
179
181
 
180
182
  The `examples/` directory contains runnable experiments. Copy a project's source into `examples/` first (gitignored), then run:
@@ -280,3 +282,17 @@ This means the server and client don't need to exchange the registry — as long
280
282
  - `hydrate.js` is browser-only code (no linkedom dependency) — it's exported separately via `@symbo.ls/brender/hydrate`
281
283
  - `createEnv()` sets `globalThis.window/document/Node/HTMLElement` because `@domql/utils` `isDOMNode` uses `instanceof` checks against global constructors
282
284
  - `onRender` callbacks that do network requests or call `s.update()` will error during SSR — this is expected and harmless since the HTML is already produced before those callbacks fire
285
+
286
+ ## Theme support
287
+
288
+ Brender defaults to `globalTheme: 'auto'`, generating CSS with both `prefers-color-scheme` media queries and `[data-theme]` selectors. The SSR output includes theme-switching CSS variables that work without JavaScript:
289
+
290
+ ```css
291
+ @media (prefers-color-scheme: dark) {
292
+ :root:not([data-theme]) { --theme-document-background: #000; }
293
+ }
294
+ [data-theme="dark"] { --theme-document-background: #000; }
295
+ [data-theme="ocean"] { --theme-document-background: #0a2e4e; }
296
+ ```
297
+
298
+ Custom themes beyond dark/light are activated via `data-theme` attribute on the root element.
package/dist/cjs/index.js CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  default: () => index_default,
24
24
  extractMetadata: () => import_metadata.extractMetadata,
25
25
  generateHeadHtml: () => import_metadata.generateHeadHtml,
26
+ generateSitemap: () => import_sitemap.generateSitemap,
26
27
  hydrate: () => import_hydrate.hydrate,
27
28
  loadAndRenderAll: () => import_load.loadAndRenderAll,
28
29
  loadProject: () => import_load.loadProject,
@@ -40,6 +41,7 @@ var import_load = require("./load.js");
40
41
  var import_render = require("./render.js");
41
42
  var import_metadata = require("./metadata.js");
42
43
  var import_hydrate = require("./hydrate.js");
44
+ var import_sitemap = require("./sitemap.js");
43
45
  var index_default = {
44
46
  createEnv: import_env.createEnv,
45
47
  resetKeys: import_keys.resetKeys,
@@ -54,5 +56,6 @@ var index_default = {
54
56
  extractMetadata: import_metadata.extractMetadata,
55
57
  generateHeadHtml: import_metadata.generateHeadHtml,
56
58
  collectBrNodes: import_hydrate.collectBrNodes,
57
- hydrate: import_hydrate.hydrate
59
+ hydrate: import_hydrate.hydrate,
60
+ generateSitemap: import_sitemap.generateSitemap
58
61
  };
@@ -17,86 +17,10 @@ var __copyProps = (to, from, except, desc) => {
17
17
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
18
  var metadata_exports = {};
19
19
  __export(metadata_exports, {
20
- extractMetadata: () => extractMetadata,
21
- generateHeadHtml: () => generateHeadHtml
20
+ applyMetadata: () => import_helmet.applyMetadata,
21
+ extractMetadata: () => import_helmet.extractMetadata,
22
+ generateHeadHtml: () => import_helmet.generateHeadHtml,
23
+ resolveMetadata: () => import_helmet.resolveMetadata
22
24
  });
23
25
  module.exports = __toCommonJS(metadata_exports);
24
- const extractMetadata = (data, route = "/") => {
25
- const pages = data.pages || {};
26
- const page = pages[route];
27
- let metadata = {};
28
- if (data.integrations?.seo) {
29
- metadata = { ...data.integrations.seo };
30
- }
31
- if (page) {
32
- const pageMeta = page.metadata || page.helmet || {};
33
- metadata = { ...metadata, ...pageMeta };
34
- if (!metadata.title && page.state?.title) {
35
- metadata.title = page.state.title;
36
- }
37
- if (!metadata.description && page.state?.description) {
38
- metadata.description = page.state.description;
39
- }
40
- }
41
- if (!metadata.title) {
42
- metadata.title = data.name || "Symbols";
43
- }
44
- return metadata;
45
- };
46
- const generateHeadHtml = (metadata) => {
47
- const esc = (text) => {
48
- if (text === null || text === void 0) return "";
49
- const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
50
- return text.toString().replace(/[&<>"']/g, (m) => map[m]);
51
- };
52
- const tags = [
53
- '<meta charset="UTF-8">',
54
- '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
55
- ];
56
- for (const [key, value] of Object.entries(metadata)) {
57
- if (!value && value !== 0 && value !== false) continue;
58
- if (key === "title") {
59
- tags.push(`<title>${esc(value)}</title>`);
60
- continue;
61
- }
62
- if (key === "canonical") {
63
- tags.push(`<link rel="canonical" href="${esc(value)}">`);
64
- continue;
65
- }
66
- if (key === "alternate" && Array.isArray(value)) {
67
- value.forEach((alt) => {
68
- if (typeof alt === "object") {
69
- const attrs = Object.entries(alt).map(([k, v]) => `${k}="${esc(v)}"`).join(" ");
70
- tags.push(`<link rel="alternate" ${attrs}>`);
71
- }
72
- });
73
- continue;
74
- }
75
- const propertyPrefixes = ["og:", "article:", "product:", "fb:", "profile:", "book:", "business:", "music:", "video:"];
76
- const namePrefixes = ["twitter:", "DC:", "DCTERMS:"];
77
- const isProperty = propertyPrefixes.some((p) => key.startsWith(p));
78
- const isName = namePrefixes.some((p) => key.startsWith(p));
79
- if (key.startsWith("http-equiv:")) {
80
- const httpKey = key.replace("http-equiv:", "");
81
- tags.push(`<meta http-equiv="${esc(httpKey)}" content="${esc(value)}">`);
82
- } else if (key.startsWith("itemprop:")) {
83
- const itemKey = key.replace("itemprop:", "");
84
- tags.push(`<meta itemprop="${esc(itemKey)}" content="${esc(value)}">`);
85
- } else if (isProperty) {
86
- if (Array.isArray(value)) {
87
- value.forEach((v) => tags.push(`<meta property="${esc(key)}" content="${esc(v)}">`));
88
- } else {
89
- tags.push(`<meta property="${esc(key)}" content="${esc(value)}">`);
90
- }
91
- } else if (isName) {
92
- tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
93
- } else if (key !== "favicon" && key !== "favicons") {
94
- if (Array.isArray(value)) {
95
- value.forEach((v) => tags.push(`<meta name="${esc(key)}" content="${esc(v)}">`));
96
- } else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
97
- tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
98
- }
99
- }
100
- }
101
- return tags.join("\n");
102
- };
26
+ var import_helmet = require("@symbo.ls/helmet");
@@ -30,9 +30,11 @@ __export(render_exports, {
30
30
  render: () => render,
31
31
  renderElement: () => renderElement,
32
32
  renderPage: () => renderPage,
33
- renderRoute: () => renderRoute
33
+ renderRoute: () => renderRoute,
34
+ resetGlobalCSSCache: () => resetGlobalCSSCache
34
35
  });
35
36
  module.exports = __toCommonJS(render_exports);
37
+ var import_path = require("path");
36
38
  var import_env = require("./env.js");
37
39
  var import_keys = require("./keys.js");
38
40
  var import_metadata = require("./metadata.js");
@@ -92,7 +94,15 @@ const UIKIT_STUBS = {
92
94
  H3: { tag: "h3" },
93
95
  H4: { tag: "h4" },
94
96
  H5: { tag: "h5" },
95
- H6: { tag: "h6" }
97
+ H6: { tag: "h6" },
98
+ Svg: {
99
+ tag: "svg",
100
+ attr: {
101
+ xmlns: "http://www.w3.org/2000/svg",
102
+ "xmlns:xlink": "http://www.w3.org/1999/xlink"
103
+ }
104
+ },
105
+ Text: { tag: "span" }
96
106
  };
97
107
  const render = async (data, options = {}) => {
98
108
  const { route = "/", state: stateOverrides, context: contextOverrides } = options;
@@ -150,9 +160,169 @@ const renderElement = async (elementDef, options = {}) => {
150
160
  }
151
161
  (0, import_keys.assignKeys)(body);
152
162
  const registry = element ? (0, import_keys.mapKeysToElements)(element) : {};
153
- const html = body.innerHTML;
163
+ const html = fixSvgContent(body.innerHTML);
154
164
  return { html, registry, element };
155
165
  };
166
+ const fixSvgContent = (html) => {
167
+ return html.replace(
168
+ /(<svg\b[^>]*>)([\s\S]*?)(<\/svg>)/gi,
169
+ (match, open, content, close) => {
170
+ if (content.includes("&lt;")) {
171
+ const unescaped = content.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
172
+ return open + unescaped + close;
173
+ }
174
+ return match;
175
+ }
176
+ );
177
+ };
178
+ let _cachedGlobalCSS = null;
179
+ const generateGlobalCSS = async (ds, config) => {
180
+ if (_cachedGlobalCSS) return _cachedGlobalCSS;
181
+ try {
182
+ const { existsSync, writeFileSync, unlinkSync } = await import("fs");
183
+ const { tmpdir } = await import("os");
184
+ const { randomBytes } = await import("crypto");
185
+ const esbuild = await import("esbuild");
186
+ const dsJson = JSON.stringify(ds || {});
187
+ const cfgJson = JSON.stringify(config || {});
188
+ const tmpEntry = (0, import_path.join)(tmpdir(), `br_global_${randomBytes(6).toString("hex")}.mjs`);
189
+ const tmpOut = (0, import_path.join)(tmpdir(), `br_global_${randomBytes(6).toString("hex")}_out.mjs`);
190
+ writeFileSync(tmpEntry, `
191
+ import { set, getActiveConfig, getFontFaceString } from '@symbo.ls/scratch'
192
+ import { DEFAULT_CONFIG } from '@symbo.ls/default-config'
193
+
194
+ const ds = ${dsJson}
195
+ const cfg = ${cfgJson}
196
+
197
+ // Merge with defaults (same as initEmotion)
198
+ const merged = {}
199
+ for (const k in DEFAULT_CONFIG) merged[k] = DEFAULT_CONFIG[k]
200
+ for (const k in ds) {
201
+ if (typeof ds[k] === 'object' && !Array.isArray(ds[k]) && typeof merged[k] === 'object' && !Array.isArray(merged[k])) {
202
+ merged[k] = { ...merged[k], ...ds[k] }
203
+ } else {
204
+ merged[k] = ds[k]
205
+ }
206
+ }
207
+
208
+ const conf = set({
209
+ useReset: true,
210
+ useVariable: true,
211
+ useFontImport: true,
212
+ useDocumentTheme: true,
213
+ useDefaultConfig: true,
214
+ globalTheme: 'auto',
215
+ ...merged,
216
+ ...cfg
217
+ }, { newConfig: {} })
218
+
219
+ const result = {
220
+ CSS_VARS: conf.CSS_VARS || {},
221
+ CSS_MEDIA_VARS: conf.CSS_MEDIA_VARS || {},
222
+ RESET: conf.RESET || conf.reset || {},
223
+ ANIMATION: conf.animation || conf.ANIMATION || {}
224
+ }
225
+ // Export as globalThis so we can read it
226
+ globalThis.__BR_GLOBAL_CSS__ = result
227
+ export default result
228
+ `);
229
+ const brenderDir = new URL(".", import_meta.url).pathname;
230
+ const monorepoRoot = (0, import_path.resolve)(brenderDir, "../..");
231
+ const workspacePlugin = {
232
+ name: "workspace-resolve",
233
+ setup(build) {
234
+ build.onResolve({ filter: /^@symbo\.ls\// }, (args) => {
235
+ const pkg = args.path.replace("@symbo.ls/", "");
236
+ for (const dir of ["packages", "plugins"]) {
237
+ const src = (0, import_path.resolve)(monorepoRoot, dir, pkg, "src", "index.js");
238
+ if (existsSync(src)) return { path: src };
239
+ const dist = (0, import_path.resolve)(monorepoRoot, dir, pkg, "index.js");
240
+ if (existsSync(dist)) return { path: dist };
241
+ }
242
+ const blank = (0, import_path.resolve)(monorepoRoot, "packages", "default-config", "blank", "index.js");
243
+ if (pkg === "default-config" && existsSync(blank)) return { path: blank };
244
+ });
245
+ build.onResolve({ filter: /^@domql\// }, (args) => {
246
+ const pkg = args.path.replace("@domql/", "");
247
+ const src = (0, import_path.resolve)(monorepoRoot, "packages", "domql", "packages", pkg, "src", "index.js");
248
+ if (existsSync(src)) return { path: src };
249
+ });
250
+ }
251
+ };
252
+ await esbuild.build({
253
+ entryPoints: [tmpEntry],
254
+ bundle: true,
255
+ format: "esm",
256
+ platform: "node",
257
+ outfile: tmpOut,
258
+ write: true,
259
+ logLevel: "silent",
260
+ plugins: [workspacePlugin],
261
+ external: ["fs", "path", "os", "crypto", "url", "http", "https", "stream", "util", "events", "buffer", "child_process", "worker_threads", "net", "tls", "dns", "dgram", "zlib", "assert", "querystring", "string_decoder", "readline", "perf_hooks", "async_hooks", "v8", "vm", "cluster", "inspector", "module", "process", "tty", "color-contrast-checker"]
262
+ });
263
+ const mod = await import(`file://${tmpOut}`);
264
+ const data = mod.default || {};
265
+ try {
266
+ unlinkSync(tmpEntry);
267
+ } catch {
268
+ }
269
+ try {
270
+ unlinkSync(tmpOut);
271
+ } catch {
272
+ }
273
+ const cssVars = data.CSS_VARS || {};
274
+ const cssMediaVars = data.CSS_MEDIA_VARS || {};
275
+ const reset = data.RESET || {};
276
+ const animations = data.ANIMATION || {};
277
+ const varDecls = Object.entries(cssVars).map(([k, v]) => ` ${k}: ${v}`).join(";\n");
278
+ let rootRule = varDecls ? `:root {
279
+ ${varDecls};
280
+ }` : "";
281
+ const themeVarRules = Object.entries(cssMediaVars).map(([key, vars]) => {
282
+ const decls = Object.entries(vars).map(([k, v]) => ` ${k}: ${v}`).join(";\n");
283
+ if (!decls) return "";
284
+ if (key.startsWith("@media")) {
285
+ return `${key} {
286
+ :root:not([data-theme]) {
287
+ ${decls};
288
+ }
289
+ }`;
290
+ }
291
+ return `${key} {
292
+ ${decls};
293
+ }`;
294
+ }).filter(Boolean).join("\n\n");
295
+ if (themeVarRules) rootRule += "\n\n" + themeVarRules;
296
+ const resetRules = generateResetCSS(reset);
297
+ const keyframeRules = [];
298
+ for (const name in animations) {
299
+ const frames = animations[name];
300
+ if (!frames || typeof frames !== "object") continue;
301
+ const frameRules = Object.entries(frames).map(([step, p]) => {
302
+ if (typeof p !== "object") return "";
303
+ const decls = Object.entries(p).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
304
+ return ` ${step} { ${decls}; }`;
305
+ }).join("\n");
306
+ keyframeRules.push(`@keyframes ${name} {
307
+ ${frameRules}
308
+ }`);
309
+ }
310
+ _cachedGlobalCSS = {
311
+ rootRule,
312
+ resetRules,
313
+ fontFaceCSS: "",
314
+ keyframeRules: keyframeRules.join("\n")
315
+ };
316
+ return _cachedGlobalCSS;
317
+ } catch (err) {
318
+ console.warn("generateGlobalCSS failed:", err.message, err.stack);
319
+ _cachedGlobalCSS = { rootRule: "", resetRules: "", fontFaceCSS: "", keyframeRules: "" };
320
+ return _cachedGlobalCSS;
321
+ }
322
+ };
323
+ const resetGlobalCSSCache = () => {
324
+ _cachedGlobalCSS = null;
325
+ };
156
326
  const renderRoute = async (data, options = {}) => {
157
327
  const { route = "/" } = options;
158
328
  const ds = data.designSystem || {};
@@ -182,34 +352,66 @@ const renderRoute = async (data, options = {}) => {
182
352
  emotion: emotionInstance,
183
353
  designSystem: ds
184
354
  });
355
+ const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
185
356
  return {
186
357
  html: cssDoc.body.innerHTML,
187
358
  css: extractCSS(result.element, ds),
188
- resetCss: generateResetCSS(ds.reset),
359
+ globalCSS,
360
+ resetCss: globalCSS.resetRules || generateResetCSS(ds.reset),
189
361
  fontLinks: generateFontLinks(ds),
190
362
  metadata: (0, import_metadata.extractMetadata)(data, route),
191
363
  brKeyCount: Object.keys(result.registry).length
192
364
  };
193
365
  };
194
366
  const renderPage = async (data, route = "/", options = {}) => {
195
- const { lang = "en", themeColor } = options;
367
+ const { lang = "en", themeColor, isr } = options;
196
368
  const result = await renderRoute(data, { route });
197
369
  if (!result) return null;
198
370
  const metadata = { ...result.metadata };
199
371
  if (themeColor) metadata["theme-color"] = themeColor;
200
372
  const headTags = (0, import_metadata.generateHeadHtml)(metadata);
373
+ const globalCSS = result.globalCSS || {};
374
+ let isrBody = "";
375
+ if (isr && isr.clientScript) {
376
+ const depth = route === "/" ? 0 : route.replace(/^\/|\/$/g, "").split("/").length;
377
+ const prefix = depth > 0 ? "../".repeat(depth) : "./";
378
+ isrBody = `<script type="module">
379
+ {
380
+ const brEls = document.querySelectorAll('body > :not(script):not(style)')
381
+ const observer = new MutationObserver((mutations) => {
382
+ for (const m of mutations) {
383
+ for (const node of m.addedNodes) {
384
+ if (node.nodeType === 1 && node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE' && !node.hasAttribute('data-br')) {
385
+ brEls.forEach(el => { if (el.hasAttribute('data-br') || el.querySelector('[data-br]')) el.remove() })
386
+ observer.disconnect()
387
+ return
388
+ }
389
+ }
390
+ }
391
+ })
392
+ observer.observe(document.body, { childList: true })
393
+ }
394
+ <\/script>
395
+ <script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
396
+ }
201
397
  const html = `<!DOCTYPE html>
202
398
  <html lang="${lang}">
203
399
  <head>
204
400
  ${headTags}
205
401
  ${result.fontLinks}
206
- <style>${result.resetCss}</style>
402
+ ${globalCSS.fontFaceCSS ? `<style>${globalCSS.fontFaceCSS}</style>` : ""}
403
+ <style>
404
+ ${globalCSS.rootRule || ""}
405
+ ${result.resetCss}
406
+ ${globalCSS.keyframeRules || ""}
407
+ </style>
207
408
  <style data-emotion="smbls">
208
409
  ${result.css}
209
410
  </style>
210
411
  </head>
211
412
  <body>
212
413
  ${result.html}
414
+ ${isrBody}
213
415
  </body>
214
416
  </html>`;
215
417
  return { html, route, brKeyCount: result.brKeyCount };
@@ -538,6 +740,34 @@ const getExtendsCSS = (el) => {
538
740
  }
539
741
  return null;
540
742
  };
743
+ const resolveElementProps = (el) => {
744
+ const { props } = el;
745
+ if (!props) return props;
746
+ let resolved;
747
+ for (const key in props) {
748
+ if (typeof props[key] !== "function") continue;
749
+ if (NON_CSS_PROPS.has(key)) continue;
750
+ if (key.charCodeAt(0) >= 65 && key.charCodeAt(0) <= 90) continue;
751
+ if (key.startsWith("on")) continue;
752
+ if (!resolved) resolved = { ...props };
753
+ let result;
754
+ try {
755
+ result = props[key](el, el.state || {});
756
+ } catch {
757
+ try {
758
+ const mockState = { root: {}, ...el.state || {} };
759
+ result = props[key](el, mockState);
760
+ } catch {
761
+ }
762
+ }
763
+ if (result !== void 0 && result !== null && result !== false) {
764
+ resolved[key] = result;
765
+ } else {
766
+ delete resolved[key];
767
+ }
768
+ }
769
+ return resolved || props;
770
+ };
541
771
  const extractCSS = (element, ds) => {
542
772
  const mediaMap = ds?.media || {};
543
773
  const animations = ds?.animation || {};
@@ -546,7 +776,7 @@ const extractCSS = (element, ds) => {
546
776
  const usedAnimations = /* @__PURE__ */ new Set();
547
777
  const walk = (el) => {
548
778
  if (!el || !el.__ref) return;
549
- const { props } = el;
779
+ const props = resolveElementProps(el);
550
780
  if (props && el.node) {
551
781
  const cls = el.node.getAttribute?.("class");
552
782
  if (cls && !seen.has(cls)) {
@@ -568,7 +798,7 @@ const extractCSS = (element, ds) => {
568
798
  }
569
799
  }
570
800
  }
571
- if (el.__ref.__children) {
801
+ if (el.__ref?.__children) {
572
802
  for (const ck of el.__ref.__children) {
573
803
  if (el[ck]?.__ref) walk(el[ck]);
574
804
  }
@@ -593,8 +823,20 @@ const generateResetCSS = (reset) => {
593
823
  const rules = [];
594
824
  for (const [selector, props] of Object.entries(reset)) {
595
825
  if (!props || typeof props !== "object") continue;
596
- const decls = Object.entries(props).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
597
- if (decls) rules.push(`${selector} { ${decls}; }`);
826
+ const baseDecls = [];
827
+ const mediaRules = [];
828
+ for (const [k, v] of Object.entries(props)) {
829
+ if (typeof v === "object" && v !== null) {
830
+ if (k.startsWith("@media") || k.startsWith("@")) {
831
+ const inner = Object.entries(v).filter(([, iv]) => typeof iv !== "object").map(([ik, iv]) => `${camelToKebab(ik)}: ${iv}`).join("; ");
832
+ if (inner) mediaRules.push(`${k} { ${selector} { ${inner}; } }`);
833
+ }
834
+ continue;
835
+ }
836
+ baseDecls.push(`${camelToKebab(k)}: ${v}`);
837
+ }
838
+ if (baseDecls.length) rules.push(`${selector} { ${baseDecls.join("; ")}; }`);
839
+ rules.push(...mediaRules);
598
840
  }
599
841
  return rules.join("\n");
600
842
  };
@@ -0,0 +1,41 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var sitemap_exports = {};
19
+ __export(sitemap_exports, {
20
+ generateSitemap: () => generateSitemap
21
+ });
22
+ module.exports = __toCommonJS(sitemap_exports);
23
+ function generateSitemap(baseUrl, routes) {
24
+ const urls = Object.entries(routes).map(([path, config]) => {
25
+ const metadata = config.metadata || {};
26
+ const canonical = metadata.canonical || `${baseUrl}${path === "/" ? "" : path}`;
27
+ return `
28
+ <url>
29
+ <loc>${canonical}</loc>
30
+ <lastmod>${(/* @__PURE__ */ new Date()).toISOString()}</lastmod>
31
+ <changefreq>weekly</changefreq>
32
+ <priority>${path === "/" ? "1.0" : "0.8"}</priority>
33
+ </url>`;
34
+ });
35
+ return `<?xml version="1.0" encoding="UTF-8"?>
36
+ <urlset
37
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
38
+ xmlns:xhtml="http://www.w3.org/1999/xhtml">
39
+ ${urls.join("\n")}
40
+ </urlset>`;
41
+ }
package/dist/esm/index.js CHANGED
@@ -4,6 +4,7 @@ import { loadProject, loadAndRenderAll } from "./load.js";
4
4
  import { render, renderElement, renderRoute, renderPage } from "./render.js";
5
5
  import { extractMetadata, generateHeadHtml } from "./metadata.js";
6
6
  import { collectBrNodes, hydrate } from "./hydrate.js";
7
+ import { generateSitemap } from "./sitemap.js";
7
8
  var index_default = {
8
9
  createEnv,
9
10
  resetKeys,
@@ -18,7 +19,8 @@ var index_default = {
18
19
  extractMetadata,
19
20
  generateHeadHtml,
20
21
  collectBrNodes,
21
- hydrate
22
+ hydrate,
23
+ generateSitemap
22
24
  };
23
25
  export {
24
26
  assignKeys,
@@ -27,6 +29,7 @@ export {
27
29
  index_default as default,
28
30
  extractMetadata,
29
31
  generateHeadHtml,
32
+ generateSitemap,
30
33
  hydrate,
31
34
  loadAndRenderAll,
32
35
  loadProject,
@@ -1,83 +1,7 @@
1
- const extractMetadata = (data, route = "/") => {
2
- const pages = data.pages || {};
3
- const page = pages[route];
4
- let metadata = {};
5
- if (data.integrations?.seo) {
6
- metadata = { ...data.integrations.seo };
7
- }
8
- if (page) {
9
- const pageMeta = page.metadata || page.helmet || {};
10
- metadata = { ...metadata, ...pageMeta };
11
- if (!metadata.title && page.state?.title) {
12
- metadata.title = page.state.title;
13
- }
14
- if (!metadata.description && page.state?.description) {
15
- metadata.description = page.state.description;
16
- }
17
- }
18
- if (!metadata.title) {
19
- metadata.title = data.name || "Symbols";
20
- }
21
- return metadata;
22
- };
23
- const generateHeadHtml = (metadata) => {
24
- const esc = (text) => {
25
- if (text === null || text === void 0) return "";
26
- const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
27
- return text.toString().replace(/[&<>"']/g, (m) => map[m]);
28
- };
29
- const tags = [
30
- '<meta charset="UTF-8">',
31
- '<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
32
- ];
33
- for (const [key, value] of Object.entries(metadata)) {
34
- if (!value && value !== 0 && value !== false) continue;
35
- if (key === "title") {
36
- tags.push(`<title>${esc(value)}</title>`);
37
- continue;
38
- }
39
- if (key === "canonical") {
40
- tags.push(`<link rel="canonical" href="${esc(value)}">`);
41
- continue;
42
- }
43
- if (key === "alternate" && Array.isArray(value)) {
44
- value.forEach((alt) => {
45
- if (typeof alt === "object") {
46
- const attrs = Object.entries(alt).map(([k, v]) => `${k}="${esc(v)}"`).join(" ");
47
- tags.push(`<link rel="alternate" ${attrs}>`);
48
- }
49
- });
50
- continue;
51
- }
52
- const propertyPrefixes = ["og:", "article:", "product:", "fb:", "profile:", "book:", "business:", "music:", "video:"];
53
- const namePrefixes = ["twitter:", "DC:", "DCTERMS:"];
54
- const isProperty = propertyPrefixes.some((p) => key.startsWith(p));
55
- const isName = namePrefixes.some((p) => key.startsWith(p));
56
- if (key.startsWith("http-equiv:")) {
57
- const httpKey = key.replace("http-equiv:", "");
58
- tags.push(`<meta http-equiv="${esc(httpKey)}" content="${esc(value)}">`);
59
- } else if (key.startsWith("itemprop:")) {
60
- const itemKey = key.replace("itemprop:", "");
61
- tags.push(`<meta itemprop="${esc(itemKey)}" content="${esc(value)}">`);
62
- } else if (isProperty) {
63
- if (Array.isArray(value)) {
64
- value.forEach((v) => tags.push(`<meta property="${esc(key)}" content="${esc(v)}">`));
65
- } else {
66
- tags.push(`<meta property="${esc(key)}" content="${esc(value)}">`);
67
- }
68
- } else if (isName) {
69
- tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
70
- } else if (key !== "favicon" && key !== "favicons") {
71
- if (Array.isArray(value)) {
72
- value.forEach((v) => tags.push(`<meta name="${esc(key)}" content="${esc(v)}">`));
73
- } else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
74
- tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
75
- }
76
- }
77
- }
78
- return tags.join("\n");
79
- };
1
+ import { extractMetadata, generateHeadHtml, resolveMetadata, applyMetadata } from "@symbo.ls/helmet";
80
2
  export {
3
+ applyMetadata,
81
4
  extractMetadata,
82
- generateHeadHtml
5
+ generateHeadHtml,
6
+ resolveMetadata
83
7
  };