initial commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
/* ============================================================================
|
||||
CMS ACCESS - server only
|
||||
----------------------------------------------------------------------------
|
||||
`.server.ts`, so the bundler keeps this (and the Strapi token behind it) out
|
||||
of the browser build entirely. Call it from a `loader` / `action`.
|
||||
|
||||
Everything here is deliberately forgiving. The CMS is empty today - and in
|
||||
development it is often not running at all - so a page must render its
|
||||
approved draft copy rather than a 500 when Strapi answers 404, times out or
|
||||
refuses the connection. `loadSingle` / `loadMany` therefore turn any
|
||||
transport- or Strapi-level failure into `null` / `[]` and log it once,
|
||||
server-side. A genuinely missing *entity* on a $slug route is a different
|
||||
thing: use `requireEntity()` from ~/lib/strapi for that, so the root
|
||||
ErrorBoundary can render the localised 404.
|
||||
========================================================================= */
|
||||
|
||||
import {
|
||||
getMany,
|
||||
getSingle,
|
||||
isStrapiError,
|
||||
mediaAlt,
|
||||
mediaFormatUrl,
|
||||
type StrapiMedia,
|
||||
type StrapiQuery,
|
||||
} from "~/lib/strapi";
|
||||
|
||||
import type { CmsPartner } from "./cms-types";
|
||||
|
||||
/** A picture, resolved to absolute URLs at load time. */
|
||||
export interface ResolvedImage {
|
||||
readonly src: string;
|
||||
readonly alt: string;
|
||||
readonly width?: number;
|
||||
readonly height?: number;
|
||||
}
|
||||
|
||||
/** A partner, flattened to exactly what the partner grid renders. */
|
||||
export interface ResolvedPartner {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly website?: string;
|
||||
readonly logo?: ResolvedImage;
|
||||
}
|
||||
|
||||
function warn(resource: string, error: unknown): void {
|
||||
const reason = isStrapiError(error)
|
||||
? `${error.status || "network"} ${error.message}`
|
||||
: String(error);
|
||||
console.warn(`[cms] "${resource}" is unavailable - rendering fallback copy (${reason})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry of a single type, or `null` when the CMS has none / is unreachable.
|
||||
*/
|
||||
export async function loadSingle<T>(
|
||||
resource: string,
|
||||
query?: StrapiQuery,
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return await getSingle<T>(resource, query);
|
||||
} catch (error) {
|
||||
warn(resource, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** A collection page, or `[]` when the CMS has none / is unreachable. */
|
||||
export async function loadMany<T>(
|
||||
resource: string,
|
||||
query?: StrapiQuery,
|
||||
): Promise<T[]> {
|
||||
try {
|
||||
const { data } = await getMany<T>(resource, query);
|
||||
return data;
|
||||
} catch (error) {
|
||||
warn(resource, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Strapi media entity as a plain, absolute-URL image.
|
||||
*
|
||||
* `format` picks the responsive derivative Strapi generated; it silently falls
|
||||
* back to the original when that derivative does not exist (small uploads).
|
||||
* `alt` comes from the CMS and stays "" when the editor left it empty, which is
|
||||
* the correct markup for a decorative image.
|
||||
*/
|
||||
export function toImage(
|
||||
media: StrapiMedia | null | undefined,
|
||||
format: "thumbnail" | "small" | "medium" | "large" = "large",
|
||||
): ResolvedImage | undefined {
|
||||
if (!media) return undefined;
|
||||
|
||||
const src = mediaFormatUrl(media, format);
|
||||
if (!src) return undefined;
|
||||
|
||||
return {
|
||||
src,
|
||||
alt: mediaAlt(media),
|
||||
...(media.width ? { width: media.width } : {}),
|
||||
...(media.height ? { height: media.height } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Flatten a populated partner relation for the grid. */
|
||||
export function toPartner(partner: CmsPartner): ResolvedPartner {
|
||||
return {
|
||||
id: partner.documentId ?? String(partner.id),
|
||||
name: partner.name,
|
||||
...(partner.website ? { website: partner.website } : {}),
|
||||
...(toImage(partner.logo ?? partner.logoDark, "small")
|
||||
? { logo: toImage(partner.logo ?? partner.logoDark, "small") }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A Strapi `json` field authored as a list of strings ("Dialogue",
|
||||
* "Inclusion", ...). Anything else in the field is ignored rather than thrown
|
||||
* over, because an editor can put arbitrary JSON there.
|
||||
*/
|
||||
export function toStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((item): item is string => typeof item === "string" && item.trim() !== "");
|
||||
}
|
||||
|
||||
/** Drop `null`/`undefined` and empty arrays in one expression. */
|
||||
export function list<T>(value: readonly T[] | null | undefined): T[] {
|
||||
return value ? [...value] : [];
|
||||
}
|
||||
Reference in New Issue
Block a user