perf(cms): batch layout bootstrap, sparse fieldsets, depth caps
Adopt RustyCMS' new resolve semantics (_depth, _fields,
POST /_batch) where they pay off:
- +layout.server.ts: 4 sequential GETs (navHeader, navSocial,
footer, page_config) collapsed into one batchFetch() call.
Legacy "default" page_config slug stays as fallback Single-
Batch only when the canonical slug is missing.
- New getPageStubs() / getPostStubs() use _fields= so the
Slug→Label/Slug→URL maps don't pull body/rows. Typically
10-50× smaller payload, eligible for the same TTL cache.
- loadPostsList() now requests posts with _depth=2 and a
sparse fieldset that keeps card-level fields plus
postImage.* and postTag.{name,icon,color,_slug}. Body
Markdown stays out of the listing entirely.
- Page + post detail routes set _depth=3 as a cycle/blast-radius
cap (page → block → image → img reaches in three levels).
- cms.ts: ContentGetOptions gains depth/fields, getPageBySlug /
getPostBySlug / getPosts thread them through (cache key
extended). New batchFetch() / batchData<T>() helpers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,14 +3,19 @@ import type { LayoutServerLoad } from './$types';
|
||||
export const trailingSlash = 'always';
|
||||
|
||||
import {
|
||||
batchFetch,
|
||||
batchData,
|
||||
getNavigationByKey,
|
||||
NavigationKeys,
|
||||
getPages,
|
||||
getPosts,
|
||||
getFooterBySlug,
|
||||
getPageConfigBySlug,
|
||||
getPageStubs,
|
||||
getPostStubs,
|
||||
} from '$lib/cms';
|
||||
import type {
|
||||
NavLinkEntry,
|
||||
NavigationEntry,
|
||||
FooterEntry,
|
||||
PageConfigEntry,
|
||||
} from '$lib/cms';
|
||||
import type { NavLinkEntry } from '$lib/cms';
|
||||
import { ensureTransformedImage } from '$lib/rusty-image';
|
||||
import {
|
||||
DEFAULT_SOCIAL_IMAGE_URL,
|
||||
@@ -45,15 +50,82 @@ function getSlugFromEntry(entry: NavLinkEntry): string {
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
const translations = locals.translations ?? {};
|
||||
|
||||
// Header: Navigation laden
|
||||
// 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
|
||||
// _fields), weil sie viel größere Listen sind und einen eigenen TTL-Cache
|
||||
// verdienen.
|
||||
let bootstrapNavHeader: NavigationEntry | null = null;
|
||||
let bootstrapNavSocial: NavigationEntry | null = null;
|
||||
let bootstrapFooter: FooterEntry | null = null;
|
||||
let bootstrapConfig: PageConfigEntry | null = null;
|
||||
try {
|
||||
const batch = await batchFetch([
|
||||
{
|
||||
id: 'navHeader',
|
||||
collection: 'navigation',
|
||||
slug: NavigationKeys.header,
|
||||
locale: 'de',
|
||||
resolve: 'links',
|
||||
},
|
||||
{
|
||||
id: 'navSocial',
|
||||
collection: 'navigation',
|
||||
slug: NavigationKeys.socialMedia,
|
||||
locale: 'de',
|
||||
resolve: 'all',
|
||||
},
|
||||
{
|
||||
id: 'footer',
|
||||
collection: 'footer',
|
||||
slug: 'footer-main',
|
||||
locale: 'de',
|
||||
resolve: 'all',
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
collection: 'page_config',
|
||||
slug: 'page-config-default',
|
||||
locale: 'de',
|
||||
resolve: 'logo',
|
||||
},
|
||||
]);
|
||||
bootstrapNavHeader = batchData<NavigationEntry>(batch.results.navHeader);
|
||||
bootstrapNavSocial = batchData<NavigationEntry>(batch.results.navSocial);
|
||||
bootstrapFooter = batchData<FooterEntry>(batch.results.footer);
|
||||
bootstrapConfig = batchData<PageConfigEntry>(batch.results.config);
|
||||
} catch {
|
||||
/* CMS nicht erreichbar — alle Felder bleiben null, Page rendert ohne. */
|
||||
}
|
||||
|
||||
// Fallback für config: Slug "default" probieren (legacy), wenn die kanonische
|
||||
// Variante fehlt. Selten genug für einen weiteren Single-Call.
|
||||
if (!bootstrapConfig) {
|
||||
try {
|
||||
const fallback = await batchFetch([
|
||||
{
|
||||
id: 'config',
|
||||
collection: 'page_config',
|
||||
slug: 'default',
|
||||
locale: 'de',
|
||||
resolve: 'logo',
|
||||
},
|
||||
]);
|
||||
bootstrapConfig = batchData<PageConfigEntry>(fallback.results.config);
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
|
||||
// Header: Navigation aufbereiten (slug→label/href-Maps aus Page-/Post-Stubs)
|
||||
let headerLinks: NavLink[] = [];
|
||||
try {
|
||||
const nav = await getNavigationByKey(NavigationKeys.header, {
|
||||
locale: 'de',
|
||||
resolve: 'links',
|
||||
});
|
||||
const nav = bootstrapNavHeader;
|
||||
const rawLinks = nav?.links ?? [];
|
||||
const [pages, posts] = await Promise.all([getPages(), getPosts()]);
|
||||
const [pages, posts] = await Promise.all([
|
||||
getPageStubs({ locale: 'de' }),
|
||||
getPostStubs({ locale: 'de' }),
|
||||
]);
|
||||
const slugToLabel = new Map<string, string>();
|
||||
const slugToPageHref = new Map<string, string>();
|
||||
for (const p of pages) {
|
||||
@@ -123,13 +195,16 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
// CMS nicht erreichbar
|
||||
}
|
||||
|
||||
// Social-Media-Links
|
||||
// Social-Media-Links (aus Batch übernommen — Fallback falls Batch nichts
|
||||
// gegeben hat: alter Single-GET mit eigenem TTL-Cache).
|
||||
let socialLinks: SocialLink[] = [];
|
||||
try {
|
||||
const socialNav = await getNavigationByKey(NavigationKeys.socialMedia, {
|
||||
locale: 'de',
|
||||
resolve: 'all',
|
||||
});
|
||||
const socialNav =
|
||||
bootstrapNavSocial ??
|
||||
(await getNavigationByKey(NavigationKeys.socialMedia, {
|
||||
locale: 'de',
|
||||
resolve: 'all',
|
||||
}));
|
||||
const rawSocialLinks = socialNav?.links ?? [];
|
||||
for (const entry of rawSocialLinks) {
|
||||
const asObj =
|
||||
@@ -149,28 +224,14 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
/* Social-Links optional */
|
||||
}
|
||||
|
||||
// Footer laden (mit Row-Resolve)
|
||||
let footerData = null;
|
||||
try {
|
||||
footerData = await getFooterBySlug('footer-main', {
|
||||
locale: 'de',
|
||||
resolve: ['all'],
|
||||
});
|
||||
} catch {
|
||||
// CMS nicht erreichbar
|
||||
}
|
||||
// Footer + page_config kommen direkt aus dem Batch (siehe oben).
|
||||
const footerData = bootstrapFooter;
|
||||
|
||||
// Logo und SEO-Templates aus page_config
|
||||
let logoUrl: string | null = null;
|
||||
let logoSvgHtml: string | null = null;
|
||||
let pageConfig = null;
|
||||
const pageConfig = bootstrapConfig;
|
||||
try {
|
||||
pageConfig =
|
||||
(await getPageConfigBySlug('page-config-default', {
|
||||
locale: 'de',
|
||||
resolve: 'logo',
|
||||
})) ??
|
||||
(await getPageConfigBySlug('default', { locale: 'de', resolve: 'logo' }));
|
||||
const rawLogo = pageConfig?.logo;
|
||||
const rawSrc =
|
||||
typeof rawLogo === 'string'
|
||||
|
||||
@@ -38,6 +38,9 @@ export const load: PageServerLoad = async ({ params, locals, fetch, url, setHead
|
||||
const page = await getPageBySlug(slug, {
|
||||
locale: 'de',
|
||||
resolve: PAGE_RESOLVE,
|
||||
// Hard-cap gegen Cycle-Surprises (page → image → img-Asset reicht in 3
|
||||
// Levels). Kein Body-Stripping hier — Page-Detail braucht alle Felder.
|
||||
depth: 3,
|
||||
preview,
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ export const load: PageServerLoad = async ({ params, locals, url, setHeaders })
|
||||
const post = await getPostBySlug(slug, {
|
||||
locale: 'de',
|
||||
resolve: POST_RESOLVE,
|
||||
// Hard-cap: post → block → image → img reicht in 3 Levels.
|
||||
depth: 3,
|
||||
preview,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user