feat(embed): standalone widget route /widget/<collection>/<slug>
Renders a single CMS entry as a block in isolation — no header, footer, or preview banner. Lets us share a deep link to a single widget (e.g. the live strommix gauge) for embedding or quick previews without exposing the rest of the page. - New BlockRenderer.svelte: switch over `_type` shared with ContentRows. - Generic `getEntryBySlug(collection, slug, options)` in cms.ts so any collection (live_strommix, deadline_banner, image, ...) is fetchable. - Route group `(embed)/+layout@.svelte` resets the layout chain so no Header/Footer/TopBanner inherit. `x-robots-tag: noindex`. - Optional `?preview=<token>` for drafts. Cache: short s-maxage for live, no-store for preview. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import '../../app.css';
|
||||
import '$lib/iconify-offline';
|
||||
let { children }: { children: import('svelte').Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="embed-root">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(html), :global(body) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.embed-root {
|
||||
min-height: 100vh;
|
||||
background: var(--color-page-bg, transparent);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { getEntryBySlug } from "$lib/cms";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
|
||||
const { collection, slug } = params;
|
||||
const preview = url.searchParams.get("preview") ?? undefined;
|
||||
const locale = url.searchParams.get("_locale") ?? undefined;
|
||||
|
||||
const entry = await getEntryBySlug<Record<string, unknown>>(collection, slug, {
|
||||
resolve: ["all"],
|
||||
preview,
|
||||
locale,
|
||||
});
|
||||
if (!entry) throw error(404, `Entry not found: ${collection}/${slug}`);
|
||||
|
||||
// _type is needed by BlockRenderer's switch. Server backfills it from the
|
||||
// route param so the renderer sees the same shape as inside a page row.
|
||||
if (typeof entry._type !== "string") {
|
||||
(entry as Record<string, unknown>)._type = collection;
|
||||
}
|
||||
|
||||
// Embeds: skip search engines, mild caching for public reads.
|
||||
setHeaders({
|
||||
"x-robots-tag": "noindex",
|
||||
"cache-control": preview
|
||||
? "private, no-store"
|
||||
: "public, s-maxage=60, stale-while-revalidate=300",
|
||||
});
|
||||
|
||||
return { collection, slug, entry, preview: !!preview };
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import BlockRenderer from "$lib/components/BlockRenderer.svelte";
|
||||
import type { ResolvedBlock } from "$lib/block-types";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const block = $derived(data.entry as ResolvedBlock);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.collection} / {data.slug}</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="widget-shell">
|
||||
<BlockRenderer {block} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.widget-shell {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user