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:
+168
-150
@@ -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}</title>
|
||||
{#if seoDescription}
|
||||
<meta name="description" content={seoDescription} />
|
||||
{/if}
|
||||
{#if pageData?.keywords}
|
||||
<meta name="keywords" content={pageData.keywords} />
|
||||
{/if}
|
||||
<link rel="canonical" href={canonical} />
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:title" content={seoTitle} />
|
||||
{#if seoDescription}
|
||||
<meta property="og:description" content={seoDescription} />
|
||||
{/if}
|
||||
{#if socialImageUrl}
|
||||
<meta property="og:image" content={socialImageUrl} />
|
||||
<meta property="og:image:width" content={String(SOCIAL_IMAGE_DIMENSIONS.width)} />
|
||||
<meta property="og:image:height" content={String(SOCIAL_IMAGE_DIMENSIONS.height)} />
|
||||
{/if}
|
||||
<meta property="og:locale" content="de_DE" />
|
||||
<meta property="og:site_name" content={data.siteName} />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content={canonical} />
|
||||
<meta name="twitter:title" content={seoTitle} />
|
||||
{#if seoDescription}
|
||||
<meta name="twitter:description" content={seoDescription} />
|
||||
{/if}
|
||||
{#if socialImageUrl}
|
||||
<meta name="twitter:image" content={socialImageUrl} />
|
||||
{/if}
|
||||
<!-- JSON-LD WebSite + Organization -->
|
||||
{@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}
|
||||
<script
|
||||
defer
|
||||
src={import.meta.env.PUBLIC_UMAMI_SCRIPT_URL}
|
||||
data-website-id={import.meta.env.PUBLIC_UMAMI_WEBSITE_ID}
|
||||
></script>
|
||||
{/if}
|
||||
{#if data.analyticsPm86SiteId}
|
||||
<script
|
||||
defer
|
||||
src="https://analytics.pm86.de/api/script.js"
|
||||
data-site-id={data.analyticsPm86SiteId}
|
||||
></script>
|
||||
{/if}
|
||||
{#if formbricksScript}
|
||||
{@html formbricksScript}
|
||||
{#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="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}</title>
|
||||
{#if seoDescription}
|
||||
<meta name="description" content={seoDescription} />
|
||||
{/if}
|
||||
{#if pageData?.keywords}
|
||||
<meta name="keywords" content={pageData.keywords} />
|
||||
{/if}
|
||||
<link rel="canonical" href={canonical} />
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:title" content={seoTitle} />
|
||||
{#if seoDescription}
|
||||
<meta property="og:description" content={seoDescription} />
|
||||
{/if}
|
||||
{#if socialImageUrl}
|
||||
<meta property="og:image" content={socialImageUrl} />
|
||||
<meta property="og:image:width" content={String(SOCIAL_IMAGE_DIMENSIONS.width)} />
|
||||
<meta property="og:image:height" content={String(SOCIAL_IMAGE_DIMENSIONS.height)} />
|
||||
{/if}
|
||||
<meta property="og:locale" content="de_DE" />
|
||||
<meta property="og:site_name" content={data.siteName} />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content={canonical} />
|
||||
<meta name="twitter:title" content={seoTitle} />
|
||||
{#if seoDescription}
|
||||
<meta name="twitter:description" content={seoDescription} />
|
||||
{/if}
|
||||
{#if socialImageUrl}
|
||||
<meta name="twitter:image" content={socialImageUrl} />
|
||||
{/if}
|
||||
<!-- JSON-LD WebSite + Organization -->
|
||||
{@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}
|
||||
<script
|
||||
defer
|
||||
src={import.meta.env.PUBLIC_UMAMI_SCRIPT_URL}
|
||||
data-website-id={import.meta.env.PUBLIC_UMAMI_WEBSITE_ID}
|
||||
></script>
|
||||
{/if}
|
||||
{#if data.analyticsPm86SiteId}
|
||||
<script
|
||||
defer
|
||||
src="https://analytics.pm86.de/api/script.js"
|
||||
data-site-id={data.analyticsPm86SiteId}
|
||||
></script>
|
||||
{/if}
|
||||
{#if formbricksScript}
|
||||
{@html formbricksScript}
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<TranslationProvider translations={data.translations}>
|
||||
<a
|
||||
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"
|
||||
>Zum Inhalt springen</a>
|
||||
<div class="flex min-h-screen flex-col text-stein-900">
|
||||
{#if pageData?.preview || pageData?.previewDraft}
|
||||
<div
|
||||
class="sticky top-0 z-[90] w-full bg-erde-500 text-black shadow-sm"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="container-custom flex items-center justify-between gap-3 py-1.5 text-sm">
|
||||
<span class="font-semibold">
|
||||
{pageData?.previewDraft ? 'Live-Preview · ungespeicherter Draft' : 'Preview-Modus · Draft sichtbar (nicht indexiert)'}
|
||||
</span>
|
||||
<a
|
||||
href={$page.url.pathname}
|
||||
class="underline hover:no-underline"
|
||||
rel="nofollow"
|
||||
>
|
||||
Preview verlassen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="print:hidden">
|
||||
<Header
|
||||
links={data.headerLinks}
|
||||
socialLinks={data.socialLinks}
|
||||
logoUrl={data.logoUrl}
|
||||
logoSvgHtml={data.logoSvgHtml}
|
||||
translations={data.translations}
|
||||
/>
|
||||
<HeaderOverlay />
|
||||
</div>
|
||||
|
||||
{#if topBannerData}
|
||||
<div class="print:hidden">
|
||||
<TopBanner
|
||||
banner={topBannerData.banner as import('$lib/cms').FullwidthBannerEntry | null}
|
||||
resolvedImages={topBannerData.resolvedImages}
|
||||
responsive={topBannerData.responsive ?? null}
|
||||
focalCss={topBannerData.focalCss ?? null}
|
||||
headline={topBannerData.headline}
|
||||
subheadline={topBannerData.subheadline}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<main id="main-content" tabindex="-1" class="flex-1 min-h-[60em] print:min-h-0">
|
||||
<div class="container-custom pb-12 print:pb-0">
|
||||
{#if breadcrumbItems.length > 0}
|
||||
<div class="print:hidden">
|
||||
<Breadcrumbs items={breadcrumbItems} />
|
||||
{#if isEmbed}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<a
|
||||
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"
|
||||
>Zum Inhalt springen</a>
|
||||
<div class="flex min-h-screen flex-col text-stein-900">
|
||||
{#if pageData?.preview || pageData?.previewDraft}
|
||||
<div
|
||||
class="sticky top-0 z-[90] w-full bg-erde-500 text-black shadow-sm"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="container-custom flex items-center justify-between gap-3 py-1.5 text-sm">
|
||||
<span class="font-semibold">
|
||||
{pageData?.previewDraft ? 'Live-Preview · ungespeicherter Draft' : 'Preview-Modus · Draft sichtbar (nicht indexiert)'}
|
||||
</span>
|
||||
<a
|
||||
href={$page.url.pathname}
|
||||
class="underline hover:no-underline"
|
||||
rel="nofollow"
|
||||
>
|
||||
Preview verlassen
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="print:hidden">
|
||||
<Header
|
||||
links={data.headerLinks}
|
||||
socialLinks={data.socialLinks}
|
||||
logoUrl={data.logoUrl}
|
||||
logoSvgHtml={data.logoSvgHtml}
|
||||
translations={data.translations}
|
||||
/>
|
||||
<HeaderOverlay />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="print:hidden">
|
||||
<Footer footer={data.footerData} translations={data.translations} />
|
||||
{#if topBannerData}
|
||||
<div class="print:hidden">
|
||||
<TopBanner
|
||||
banner={topBannerData.banner as import('$lib/cms').FullwidthBannerEntry | null}
|
||||
resolvedImages={topBannerData.resolvedImages}
|
||||
responsive={topBannerData.responsive ?? null}
|
||||
focalCss={topBannerData.focalCss ?? null}
|
||||
headline={topBannerData.headline}
|
||||
subheadline={topBannerData.subheadline}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<main id="main-content" tabindex="-1" class="flex-1 min-h-[60em] print:min-h-0">
|
||||
<div class="container-custom pb-12 print:pb-0">
|
||||
{#if breadcrumbItems.length > 0}
|
||||
<div class="print:hidden">
|
||||
<Breadcrumbs items={breadcrumbItems} />
|
||||
</div>
|
||||
{/if}
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="print:hidden">
|
||||
<Footer footer={data.footerData} translations={data.translations} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</TranslationProvider>
|
||||
|
||||
Reference in New Issue
Block a user