bosia 0.9.2 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
@@ -394,18 +394,29 @@
394
394
 
395
395
  settleScroll();
396
396
 
397
- // Update document title and meta description from server metadata
398
- if (result?.metadata) {
399
- if (result.metadata.title) document.title = result.metadata.title;
400
- if (result.metadata.description) {
401
- let meta = document.querySelector('meta[name="description"]') as HTMLMetaElement | null;
402
- if (!meta) {
403
- meta = document.createElement("meta");
404
- meta.name = "description";
405
- document.head.appendChild(meta);
406
- }
407
- meta.content = result.metadata.description;
408
- }
397
+ // Re-emit the head from server metadata. `data-bosia-meta` (html.ts `OWNED`)
398
+ // marks exactly the tags metadata() owns, so headExtras, the framework's own
399
+ // static tags and <svelte:head> output are never touched. `result === null`
400
+ // with server data means the fetch failed — leave the head as it is; without
401
+ // server data there is no metadata() at all, so clearing matches SSR.
402
+ if (result || !match.route.hasServerData) {
403
+ const md = result?.metadata;
404
+ document.querySelectorAll("[data-bosia-meta]").forEach((el) => el.remove());
405
+ // A page that declares no title keeps the previous one rather than flashing
406
+ // the "Bosia App" fallback. Give metadata() a title if that matters.
407
+ if (md?.title) document.title = md.title;
408
+ if (md?.lang) document.documentElement.lang = md.lang;
409
+ const emit = (tag: "meta" | "link", attrs: Record<string, string | undefined>) => {
410
+ const el = document.createElement(tag);
411
+ for (const [k, v] of Object.entries(attrs)) if (v != null) el.setAttribute(k, v);
412
+ el.setAttribute("data-bosia-meta", "");
413
+ document.head.appendChild(el);
414
+ };
415
+ if (md?.description) emit("meta", { name: "description", content: md.description });
416
+ for (const m of md?.meta ?? [])
417
+ emit("meta", { name: m.name, property: m.property, content: m.content });
418
+ for (const l of md?.link ?? [])
419
+ emit("link", { rel: l.rel, href: l.href, hreflang: l.hreflang });
409
420
  }
410
421
 
411
422
  settle({ url, params: match.params });
package/src/core/html.ts CHANGED
@@ -287,6 +287,11 @@ const SPINNER =
287
287
  `border-radius:50%;animation:__bs__ .8s linear infinite}` +
288
288
  `@keyframes __bs__{to{transform:rotate(360deg)}}</style><i></i></div>`;
289
289
 
290
+ /** Marks the tags `metadata()` owns, so the client router can replace exactly
291
+ * these on navigation and leave `headExtras`, the framework's own static tags
292
+ * and `<svelte:head>` output alone. Read by `client/App.svelte`. */
293
+ export const OWNED = "data-bosia-meta";
294
+
290
295
  /** The `metadata()` tags themselves, indented head-ready. Shared by the streaming
291
296
  * path (buildMetadataChunk) and the non-streaming one (buildHtml) so the two
292
297
  * renderers cannot drift on what `metadata()` emits. */
@@ -295,21 +300,21 @@ export function metadataTags(metadata: Metadata | null): string {
295
300
  let out = "";
296
301
  if (metadata.title) out += ` <title>${escapeHtml(metadata.title)}</title>\n`;
297
302
  if (metadata.description) {
298
- out += ` <meta name="description" content="${escapeAttr(metadata.description)}">\n`;
303
+ out += ` <meta name="description" content="${escapeAttr(metadata.description)}" ${OWNED}>\n`;
299
304
  }
300
305
  if (metadata.meta) {
301
306
  for (const m of metadata.meta) {
302
307
  const attrs = m.name
303
308
  ? `name="${escapeAttr(m.name)}"`
304
309
  : `property="${escapeAttr(m.property ?? "")}"`;
305
- out += ` <meta ${attrs} content="${escapeAttr(m.content)}">\n`;
310
+ out += ` <meta ${attrs} content="${escapeAttr(m.content)}" ${OWNED}>\n`;
306
311
  }
307
312
  }
308
313
  if (metadata.link) {
309
314
  for (const l of metadata.link) {
310
315
  let attrs = `rel="${escapeAttr(l.rel)}" href="${escapeAttr(l.href)}"`;
311
316
  if (l.hreflang) attrs += ` hreflang="${escapeAttr(l.hreflang)}"`;
312
- out += ` <link ${attrs}>\n`;
317
+ out += ` <link ${attrs} ${OWNED}>\n`;
313
318
  }
314
319
  }
315
320
  return out;
@@ -303,7 +303,16 @@ async function resolve(event: RequestEvent): Promise<Response> {
303
303
  cookies,
304
304
  request,
305
305
  );
306
- if (meta) metadata = { title: meta.title, description: meta.description };
306
+ // Explicit whitelist, not a spread: `metadata.data` feeds load() on the
307
+ // server and may hold secrets — it must not reach the client.
308
+ if (meta)
309
+ metadata = {
310
+ title: meta.title,
311
+ description: meta.description,
312
+ meta: meta.meta,
313
+ link: meta.link,
314
+ lang: meta.lang,
315
+ };
307
316
  } catch {
308
317
  /* non-fatal */
309
318
  }