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:
@@ -625,6 +625,42 @@ export async function getPostBySlug(
|
||||
return cached("post", key, fetchFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic single-entry fetch by `(collection, slug)`. Used by the embed/widget
|
||||
* route that renders a single block standalone.
|
||||
*
|
||||
* Resolves nested references via `_resolve=all` by default so block components
|
||||
* see the same data shape as inside a page row.
|
||||
*/
|
||||
export async function getEntryBySlug<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
slug: string,
|
||||
options?: ContentGetOptions,
|
||||
): Promise<T | null> {
|
||||
const resolveKey = options?.resolve?.join(",") ?? "all";
|
||||
const fieldsKey = options?.fields?.join(",") ?? "";
|
||||
const depthKey = options?.depth ?? "";
|
||||
const key = `entry:${collection}:${slug}:${options?.locale ?? ""}:${resolveKey}:${depthKey}:${fieldsKey}`;
|
||||
const fetchFn = async (): Promise<T | null> => {
|
||||
const base = getBaseUrl();
|
||||
const u = new URL(`${base}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`);
|
||||
if (options?.locale) u.searchParams.set("_locale", options.locale);
|
||||
u.searchParams.set("_resolve", options?.resolve?.length ? options.resolve.join(",") : "all");
|
||||
if (typeof options?.depth === "number") u.searchParams.set("_depth", String(options.depth));
|
||||
if (options?.fields?.length) u.searchParams.set("_fields", options.fields.join(","));
|
||||
if (options?.preview) u.searchParams.set("preview", options.preview);
|
||||
if (options?.previewDraft) u.searchParams.set("preview_draft", options.previewDraft);
|
||||
const res = await cmsFetch(u.toString());
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) {
|
||||
throw new Error(`RustyCMS getEntry failed: ${res.status} for ${collection}/${slug}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
};
|
||||
if (options?.preview || options?.previewDraft) return fetchFn();
|
||||
return cached(collection, key, fetchFn);
|
||||
}
|
||||
|
||||
/** Für Post-URLs wird slug bevorzugt (nicht _slug). Führender Slash wird ignoriert. */
|
||||
export async function getPostSlugs(): Promise<string[]> {
|
||||
const items = await getPosts();
|
||||
|
||||
Reference in New Issue
Block a user