fix(embed): skip site chrome on /widget routes via root layout
(embed)/+layout@.svelte only resets intermediate layouts — root
+layout.svelte still wrapped every widget page with Header, Footer,
TopBanner, Breadcrumbs, Skip-Link, OG/JSON-LD/Analytics.
Root layout now derives isEmbed from $page.route.id (matches /(embed))
and renders just {@render children()} in that branch. svelte:head is
gated too: embed mode emits only the favicon, leaving the page free to
set its own <title> and noindex meta.
+layout.server.ts short-circuits the bootstrap batch (navigation,
footer, page_config, logo) for embed routes — saves one CMS roundtrip
+ logo transform per widget request, and avoids double-counting in
Umami/pm86 analytics when the widget is iframed elsewhere.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -48,9 +48,33 @@ function getSlugFromEntry(entry: NavLinkEntry): string {
|
|||||||
return e.slug ?? e._slug ?? '';
|
return e.slug ?? e._slug ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
export const load: LayoutServerLoad = async ({ locals, route }) => {
|
||||||
const translations = locals.translations ?? {};
|
const translations = locals.translations ?? {};
|
||||||
|
|
||||||
|
// Embed-Routen ((embed)/...) brauchen kein Site-Chrome. Bootstrap-Batch
|
||||||
|
// wird übersprungen — spart 1 CMS-Roundtrip + Logo-Transform pro Request.
|
||||||
|
// Layout-Komponente rendert über `isEmbed` ohne Header/Footer/Banner.
|
||||||
|
if ((route.id ?? '').startsWith('/(embed)')) {
|
||||||
|
const siteName =
|
||||||
|
(import.meta.env.PUBLIC_SITE_NAME as string | undefined) || SITE_NAME;
|
||||||
|
const siteBaseUrl = (env.PUBLIC_SITE_URL || '').replace(/\/$/, '');
|
||||||
|
return {
|
||||||
|
headerLinks: [] as NavLink[],
|
||||||
|
socialLinks: [] as SocialLink[],
|
||||||
|
footerData: null as FooterEntry | null,
|
||||||
|
logoUrl: null as string | null,
|
||||||
|
logoSvgHtml: null as string | null,
|
||||||
|
logoSocialImage: null as string | null,
|
||||||
|
siteName,
|
||||||
|
siteBaseUrl,
|
||||||
|
translations,
|
||||||
|
pageConfig: null as PageConfigEntry | null,
|
||||||
|
seoTitleTemplate: null,
|
||||||
|
seoDescriptionTemplate: null,
|
||||||
|
analyticsPm86SiteId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Layout-Bootstrap: Navigation (header + social), Footer, page_config in
|
// Layout-Bootstrap: Navigation (header + social), Footer, page_config in
|
||||||
// einem einzigen HTTP-Roundtrip (POST /api/content/_batch). Reduziert von
|
// einem einzigen HTTP-Roundtrip (POST /api/content/_batch). Reduziert von
|
||||||
// 4 sequentiellen Calls auf 1. Page-/Post-Stubs gehen separat (sparse
|
// 4 sequentiellen Calls auf 1. Page-/Post-Stubs gehen separat (sparse
|
||||||
|
|||||||
@@ -16,6 +16,14 @@
|
|||||||
|
|
||||||
let { data, children }: { data: LayoutData; children: import('svelte').Snippet } = $props();
|
let { data, children }: { data: LayoutData; children: import('svelte').Snippet } = $props();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Embed-Routen (z.B. /widget/...) liegen in der `(embed)`-Route-Group und
|
||||||
|
* sollen ohne Site-Chrome (Header, Footer, TopBanner, Breadcrumbs, Skip-Link,
|
||||||
|
* Preview-Banner) gerendert werden — auch SEO-Tags ausser noindex sind irrelevant,
|
||||||
|
* weil das Widget per `<iframe>` eingebettet wird.
|
||||||
|
*/
|
||||||
|
const isEmbed = $derived(($page.route.id ?? '').startsWith('/(embed)'));
|
||||||
|
|
||||||
// Live-preview reload bridge: when the page was loaded with
|
// Live-preview reload bridge: when the page was loaded with
|
||||||
// `?preview_draft=...`, accept `rustycms:reload` postMessage events from the
|
// `?preview_draft=...`, accept `rustycms:reload` postMessage events from the
|
||||||
// admin iframe parent and re-run the SvelteKit `load()` so the iframe
|
// admin iframe parent and re-run the SvelteKit `load()` so the iframe
|
||||||
@@ -175,6 +183,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
{#if isEmbed}
|
||||||
|
<!-- Embed-Modus: minimaler Head, kein OG/JSON-LD/Analytics. Page setzt
|
||||||
|
eigenen <title> und robots-meta. -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
{:else}
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<meta name="robots" content={robotsContent} />
|
<meta name="robots" content={robotsContent} />
|
||||||
{#if bannerPreload}
|
{#if bannerPreload}
|
||||||
@@ -266,9 +279,13 @@
|
|||||||
{#if formbricksScript}
|
{#if formbricksScript}
|
||||||
{@html formbricksScript}
|
{@html formbricksScript}
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<TranslationProvider translations={data.translations}>
|
<TranslationProvider translations={data.translations}>
|
||||||
|
{#if isEmbed}
|
||||||
|
{@render children()}
|
||||||
|
{:else}
|
||||||
<a
|
<a
|
||||||
href="#main-content"
|
href="#main-content"
|
||||||
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:bg-white focus:px-3 focus:py-2 focus:shadow focus:outline focus:outline-2 focus:outline-wald-600 focus:rounded-xs"
|
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:bg-white focus:px-3 focus:py-2 focus:shadow focus:outline focus:outline-2 focus:outline-wald-600 focus:rounded-xs"
|
||||||
@@ -333,4 +350,5 @@
|
|||||||
<Footer footer={data.footerData} translations={data.translations} />
|
<Footer footer={data.footerData} translations={data.translations} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user