diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e440ecf..463a738 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -49,9 +49,12 @@ export const handle: Handle = async ({ event, resolve }) => { }, }); + const isPreview = event.url.searchParams.has('preview'); + if ( event.request.method === 'GET' && isCacheablePath(event.url.pathname) && + !isPreview && !response.headers.has('cache-control') ) { response.headers.set('cache-control', PAGE_CACHE_CONTROL); @@ -64,7 +67,9 @@ export const handle: Handle = async ({ event, resolve }) => { ) { response.headers.set( 'x-robots-tag', - 'index, follow, max-image-preview:large, max-snippet:-1', + isPreview + ? 'noindex, nofollow' + : 'index, follow, max-image-preview:large, max-snippet:-1', ); } diff --git a/src/lib/cms.ts b/src/lib/cms.ts index 0fc7155..f255349 100644 --- a/src/lib/cms.ts +++ b/src/lib/cms.ts @@ -229,6 +229,8 @@ export type ContentGetOptions = { locale?: string; /** Felder, deren Referenzen aufgelöst werden (z. B. ["row1Content", "row2Content", "row3Content"]). */ resolve?: string[]; + /** Signierter Preview-Token (HMAC), entsperrt Drafts für genau diesen Eintrag. Bypass Cache. */ + preview?: string; }; /** Führenden + abschließenden Slash entfernen (CMS "slug": "/about"; Route-Param mit trailing slash). */ @@ -245,13 +247,14 @@ export async function getPageBySlug( ): Promise { const resolveKey = options?.resolve?.join(",") ?? ""; const key = `page:slug:${slug}:${options?.locale ?? ""}:${resolveKey}`; - return cached("page", key, async () => { + const fetchFn = async () => { const base = getBaseUrl(); const trySlug = async (s: string): Promise => { const u = new URL(`${base}/api/content/page/${encodeURIComponent(s)}`); if (options?.locale) u.searchParams.set("_locale", options.locale); if (options?.resolve?.length) u.searchParams.set("_resolve", options.resolve.join(",")); + if (options?.preview) u.searchParams.set("preview", options.preview); return fetch(u.toString()); }; const normSlug = normalizePageSlug(slug); @@ -277,7 +280,9 @@ export async function getPageBySlug( ); } return (await res.json()) as PageEntry; - }); + }; + if (options?.preview) return fetchFn(); + return cached("page", key, fetchFn); } /** Für Page-URLs wird `slug` bevorzugt (nicht _slug). Führender Slash wird ignoriert. */ @@ -435,13 +440,14 @@ export async function getPostBySlug( ): Promise { const resolveKey = options?.resolve?.join(",") ?? ""; const key = `post:slug:${slug}:${options?.locale ?? ""}:${resolveKey}`; - return cached("post", key, async () => { + const fetchFn = async () => { const base = getBaseUrl(); const trySlug = async (s: string): Promise => { const u = new URL(`${base}/api/content/post/${encodeURIComponent(s)}`); if (options?.locale) u.searchParams.set("_locale", options.locale); if (options?.resolve?.length) u.searchParams.set("_resolve", options.resolve.join(",")); + if (options?.preview) u.searchParams.set("preview", options.preview); return fetch(u.toString()); }; const normSlug = normalizePostSlug(slug); @@ -467,7 +473,9 @@ export async function getPostBySlug( ); } return (await res.json()) as PostEntry; - }); + }; + if (options?.preview) return fetchFn(); + return cached("post", key, fetchFn); } /** Für Post-URLs wird slug bevorzugt (nicht _slug). Führender Slash wird ignoriert. */ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9a6b42c..a7303eb 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -21,6 +21,7 @@ socialImage?: string; robots?: string; keywords?: string; + preview?: boolean; jsonLd?: Record | Record[]; breadcrumbItems?: { href?: string; label: string }[]; topBanner?: { @@ -248,6 +249,26 @@ 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-sm" >Zum Inhalt springen
+ {#if pageData?.preview} +
+
+ + Preview-Modus — Draft sichtbar (nicht indexiert) + + + Preview verlassen + +
+
+ {/if}
{ +export const load: PageServerLoad = async ({ params, locals, fetch, url, setHeaders }) => { const { slug } = params; const translations = locals.translations ?? {}; + const preview = url.searchParams.get('preview') ?? undefined; + + if (preview) { + setHeaders({ + 'cache-control': 'private, no-store, max-age=0', + 'x-robots-tag': 'noindex, nofollow', + }); + } const page = await getPageBySlug(slug, { locale: 'de', resolve: PAGE_RESOLVE, + preview, }); if (!page) { @@ -78,6 +87,7 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => { return { page, translations, + preview: !!preview, seoTitle: page.seoTitle ?? page.headline ?? page.linkName ?? slug, seoDescription: page.seoDescription ?? page.subheadline ?? '', robots: (page as { seoMetaRobots?: string }).seoMetaRobots ?? undefined, diff --git a/src/routes/post/[slug]/+page.server.ts b/src/routes/post/[slug]/+page.server.ts index de63baa..20c6b9a 100644 --- a/src/routes/post/[slug]/+page.server.ts +++ b/src/routes/post/[slug]/+page.server.ts @@ -20,13 +20,22 @@ import { marked } from 'marked'; marked.setOptions({ gfm: true }); -export const load: PageServerLoad = async ({ params, locals, url }) => { +export const load: PageServerLoad = async ({ params, locals, url, setHeaders }) => { const { slug } = params; const translations = locals.translations ?? {}; + const preview = url.searchParams.get('preview') ?? undefined; + + if (preview) { + setHeaders({ + 'cache-control': 'private, no-store, max-age=0', + 'x-robots-tag': 'noindex, nofollow', + }); + } const post = await getPostBySlug(slug, { locale: 'de', resolve: POST_RESOLVE, + preview, }); if (!post) { @@ -202,6 +211,7 @@ export const load: PageServerLoad = async ({ params, locals, url }) => { return { post, + preview: !!preview, postImageUrl, rawPostImageUrl: postImageField?.url ?? null, postImageFocal: postImageField?.focal ?? null,