From 3f62473c3215179d1ba7f62a851a132bbb27679b Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Tue, 5 May 2026 10:04:01 +0200 Subject: [PATCH] fix(embed): skip site chrome on /widget routes via root layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (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 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> --- src/routes/+layout.server.ts | 26 ++- src/routes/+layout.svelte | 318 ++++++++++++++++++----------------- 2 files changed, 193 insertions(+), 151 deletions(-) diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index b779e2a..465053f 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -48,9 +48,33 @@ function getSlugFromEntry(entry: NavLinkEntry): string { return e.slug ?? e._slug ?? ''; } -export const load: LayoutServerLoad = async ({ locals }) => { +export const load: LayoutServerLoad = async ({ locals, route }) => { 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 // einem einzigen HTTP-Roundtrip (POST /api/content/_batch). Reduziert von // 4 sequentiellen Calls auf 1. Page-/Post-Stubs gehen separat (sparse diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 66b30fd..09c4b8d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -16,6 +16,14 @@ 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 // `?preview_draft=...`, accept `rustycms:reload` postMessage events from the // admin iframe parent and re-run the SvelteKit `load()` so the iframe @@ -175,162 +183,172 @@ </script> <svelte:head> - <meta name="theme-color" content="#ffffff" /> - <meta name="robots" content={robotsContent} /> - {#if bannerPreload} - <link - rel="preload" - as="image" - href={bannerPreload.href} - imagesrcset={bannerPreload.srcset} - imagesizes="100vw" - type={bannerPreload.type} - fetchpriority="high" - /> - {/if} - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> - <link rel="alternate" type="application/rss+xml" title="Beiträge" href="/posts/rss.xml" /> - <link rel="sitemap" href="/sitemap.xml" /> - <link rel="icon" href="/favicons/favicon.ico" sizes="any" /> - <link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" /> - <link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" /> - <link rel="icon" type="image/png" sizes="96x96" href="/favicons/favicon-96x96.png" /> - <link rel="icon" type="image/png" sizes="192x192" href="/favicons/android-icon-192x192.png" /> - <link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-icon-57x57.png" /> - <link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-icon-60x60.png" /> - <link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-icon-72x72.png" /> - <link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-icon-76x76.png" /> - <link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-icon-114x114.png" /> - <link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-icon-120x120.png" /> - <link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-icon-144x144.png" /> - <link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-icon-152x152.png" /> - <link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-icon-180x180.png" /> - <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#2d7a45" /> - <meta name="msapplication-TileColor" content="#ffffff" /> - <meta name="msapplication-TileImage" content="/favicons/ms-icon-144x144.png" /> - <title>{seoTitle} - {#if seoDescription} - - {/if} - {#if pageData?.keywords} - - {/if} - - - - - - {#if seoDescription} - - {/if} - {#if socialImageUrl} - - - - {/if} - - - - - - - {#if seoDescription} - - {/if} - {#if socialImageUrl} - - {/if} - - {@html jsonLdScript} - {@html organizationLdScript} - {#if breadcrumbJsonLdScript} - {@html breadcrumbJsonLdScript} - {/if} - {#if pageJsonLdScripts} - {@html pageJsonLdScripts} - {/if} - {#if import.meta.env.PUBLIC_UMAMI_SCRIPT_URL && import.meta.env.PUBLIC_UMAMI_WEBSITE_ID} - - {/if} - {#if data.analyticsPm86SiteId} - - {/if} - {#if formbricksScript} - {@html formbricksScript} + {#if isEmbed} + + + {:else} + + + {#if bannerPreload} + + {/if} + + + + + + + + + + + + + + + + + + + + + {seoTitle} + {#if seoDescription} + + {/if} + {#if pageData?.keywords} + + {/if} + + + + + + {#if seoDescription} + + {/if} + {#if socialImageUrl} + + + + {/if} + + + + + + + {#if seoDescription} + + {/if} + {#if socialImageUrl} + + {/if} + + {@html jsonLdScript} + {@html organizationLdScript} + {#if breadcrumbJsonLdScript} + {@html breadcrumbJsonLdScript} + {/if} + {#if pageJsonLdScripts} + {@html pageJsonLdScripts} + {/if} + {#if import.meta.env.PUBLIC_UMAMI_SCRIPT_URL && import.meta.env.PUBLIC_UMAMI_WEBSITE_ID} + + {/if} + {#if data.analyticsPm86SiteId} + + {/if} + {#if formbricksScript} + {@html formbricksScript} + {/if} {/if} - Zum Inhalt springen -
- {#if pageData?.preview || pageData?.previewDraft} -
-
- - {pageData?.previewDraft ? 'Live-Preview · ungespeicherter Draft' : 'Preview-Modus · Draft sichtbar (nicht indexiert)'} - - - Preview verlassen - -
-
- {/if} -
-
- -
- - {#if topBannerData} -
- -
- {/if} - -
-
- {#if breadcrumbItems.length > 0} -
- + {#if isEmbed} + {@render children()} + {:else} + Zum Inhalt springen +
+ {#if pageData?.preview || pageData?.previewDraft} +
+
+ + {pageData?.previewDraft ? 'Live-Preview · ungespeicherter Draft' : 'Preview-Modus · Draft sichtbar (nicht indexiert)'} + + + Preview verlassen +
- {/if} - {@render children()} +
+ {/if} +
+
+
-
-
-
-
+ {/if}