Loadery serwerowe
Pobieraj dane bloku podczas renderowania po stronie serwera, żeby były indeksowalne, renderowały się bez mignięcia i nie wciągały zależności serwerowych do bundla klienta.
loader bloku działa na serwerze podczas SSR i przekazuje swój wynik do komponentu jako prop data. Użyj go, żeby pobrać treść, wykonać ciężkie transformacje albo odpytać API dostawcze, zanim strona trafi do przeglądarki - zamiast pobierać po stronie klienta w useEffect.
Wynikają z tego trzy rzeczy:
- SEO - treść jest w HTML-u wyrenderowanym na serwerze, więc crawlery ją widzą.
- Brak mignięcia - blok renderuje się wypełniony przy pierwszym malowaniu. Żadnych szkieletów.
- Mniejszy bundle klienta - zależności tylko serwerowe, jak highlighter składni czy sanitizer HTML, nigdy nie trafiają do przeglądarki.
Kontrakt
import { defineBlock } from "@cmssy/react";
defineBlock({
type: "my-block",
loader: async ({ content, context }) => {
// tylko na serwerze, podczas SSR; zwraca dane serializowalne przez RSC
return { html: "" };
},
component: MyBlock, // dostaje { content, context, data }
});Rządzą nim trzy reguły:
- Loader działa podczas SSR. Nie działa w edytorze - tam komponent dostaje
data: undefined. Zawsze renderuj sensowny fallback. - Zwracana wartość przekracza granicę serwer-klient, więc musi być serializowalna przez RSC: zwykłe obiekty, tablice i prymitywy. Żadnych funkcji, żadnych instancji klas.
contentto rozwiązana treść bloku;contextto kontekst bloku (locale,isPreview,forms).
Typuj data jako opcjonalne i degraduj się, gdy go brak:
function MyBlock({
content,
data,
}: {
content: Record<string, unknown>;
data?: { html?: string };
}) {
if (!data?.html) return <pre>{String(content.code ?? "")}</pre>; // fallback dla edytora
return <div dangerouslySetInnerHTML={{ __html: data.html }} />;
}Trzymaj kod serwerowy poza bundlem klienta
Moduł bloku jest osiągalny także z klienckiego bundla edytora. Jeśli Twój loader importuje statycznie ciężką albo serwerową zależność, ta zależność ląduje też w bundlu przeglądarki. Chronią przed tym dwa zabezpieczenia:
- Dynamiczny
import()wewnątrz loadera - zależność wciąga się dopiero, gdy loader faktycznie działa. - Runtime'owy guard na
windoww każdym współdzielonym helperze serwerowym - twardy błąd, jeśli kiedykolwiek zostanie osiągnięty na kliencie.
// block.ts
loader: async ({ content }) => {
const code = typeof content.code === "string" ? content.code : "";
if (!code) return { html: "" };
const { codeToHtml } = await import("shiki"); // serwerowe, leniwe
return { html: await codeToHtml(code, { lang: "ts", theme: "github-light" }) };
},// load-posts.ts - helper tylko serwerowy, sięgany przez dynamiczny import()
import { createCmssyClient } from "@cmssy/react";
import { cmssy } from "@/cmssy/config";
const client = createCmssyClient(cmssy);
export async function loadPosts(vars: { parentSlug: string; limit: number }) {
if (typeof window !== "undefined") {
throw new Error("loadPosts must only run on the server");
}
return client.queryScoped(PUBLIC_PAGES_QUERY, vars);
}Sam loader zostaje malutki; helper ładuje się wyłącznie na serwerze:
loader: async ({ content }) => {
const parentSlug = resolveParentSlug(content);
if (!parentSlug) return null;
const { loadPosts } = await import("./load-posts");
return loadPosts({ parentSlug, limit: Number(content.postsPerPage) || 9 });
},Odpytywanie API dostawczego
Użyj createCmssyClient(...).queryScoped(...) albo graphqlRequest z @cmssy/react.
queryScoped automatycznie wstrzykuje id workspace'u: gdy Twoje zapytanie deklaruje $workspaceId, a Ty go nie podasz, SDK rozwiąże je z workspaceSlug i doda zarówno zmienną, jak i nagłówek x-workspace-id. Nigdy nie zarządzasz id workspace'u sam.
Hybryda: pierwsza strona z SSR, potem klient
Loader nie musi trzymać danych na zawsze. Częsty wzorzec: załaduj pierwszą stronę na serwerze, zasil stan początkowy hooka klienckiego z data, a wyszukiwanie, paginację i filtrowanie zostaw klientowi:
function BlogPosts({ content, context, data }) {
// stan początkowy z `data` z SSR; resztę obsługują efekty klienckie
const state = useBlogPosts(content, context, data);
// ...
}Gdy data jest obecne, klient w ogóle pomija pierwszy fetch. Do sieci idą tylko akcje użytkownika - wpisanie frazy, przewinięcie po więcej.
Przykłady z życia
Trzy bloki w aplikacji referencyjnej cmssy-web używają loaderów, każdy z innego powodu:
docs-code-block- podkolorowanie składni na serwerze dodata.html. Zależność serwerowa:shiki.legal- sanityzacja HTML-a z CMS-a dodata.sections. Zależność serwerowa:sanitize-html.blog-posts- pobranie pierwszej strony wpisów dodata.items. Zależność serwerowa: API dostawcze.
Gdy loader rzuci wyjątkiem
Loader, który rzuci wyjątkiem, nie kładzie strony. SDK go łapie i izoluje blok:
- Na Twojej stronie - blok nie renderuje niczego. Reszta strony działa normalnie.
- W edytorze - w miejscu bloku pojawia się karta diagnostyczna z etykietą loader failed, komunikatem błędu i id bloku.
Ta asymetria jest celowa. Odwiedzający nigdy nie powinien zobaczyć stack trace'u, a osoba edytująca nigdy nie powinna zgadywać, dlaczego blok zniknął.
Znaczy to też, że padający loader łatwo przeoczyć na produkcji - strona po prostu po cichu ma o jedną sekcję mniej. Jeśli bloku brakuje na żywej stronie, a treść wygląda poprawnie, otwórz tę stronę w edytorze, zanim zaczniesz podejrzewać treść.
Lista kontrolna
- Loader zwraca dane serializowalne przez RSC - zwykłe obiekty, tablice, prymitywy.
- Komponent renderuje fallback, gdy
datajestundefined, żeby edytor dalej działał. - Zależności serwerowe siedzą za dynamicznym
import(). - Współdzielone helpery serwerowe mają guard na
typeof window !== "undefined". - Wywołania do API idą przez
queryScopedalbographqlRequest, więc workspace jest ustawiany automatycznie.
Następne kroki
- Tworzenie bloków -
defineBlockod początku do końca. - Zaawansowane funkcje bloków - kontekst, zachowanie w podglądzie, przypadki brzegowe.
- API i AI - co udostępnia API dostawcze.