134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import type { S3Object } from "@/default.sqlite.d.ts";
|
|
import type { Context } from "@bureaudouble/routeur";
|
|
import convertToPlain from "@bureaudouble/outils/convertToPlain.ts";
|
|
import { getHmrScriptPath } from "@bureaudouble/outils/routes/createHmrRouter.ts";
|
|
import { context } from "@bureaudouble/rsc/server/context";
|
|
|
|
import { database, getS3Uri } from "@/app/utils/database.ts";
|
|
import tailwind from "@/build/tailwind/snapshot.json" with { type: "json" };
|
|
import { sharp } from "@/app/components/utils/image.tsx";
|
|
|
|
const Favicons = async ({ media }: { media?: S3Object | null }) => {
|
|
if (!media) return;
|
|
const [icosRes, pngsRes] = await Promise.all(
|
|
[
|
|
{ resizes: [32], format: ["image/x-icon" as const] },
|
|
{ resizes: [180, 192, 512], format: ["image/png" as const] },
|
|
].map((options) => sharp.createResponsiveImage([media], options)),
|
|
);
|
|
const pngs = pngsRes
|
|
.flatMap((v) => v.sources)
|
|
.flatMap((v) => v?.srcSet ?? []);
|
|
const ico = icosRes.flatMap((v) => v.sources.at(-1)?.srcSet ?? []).at(0);
|
|
const svg = media["content-type"] === "image/svg+xml"
|
|
? getS3Uri(media.key).href
|
|
: null;
|
|
const manifest = "data:application/json;base64,".concat(
|
|
btoa(
|
|
JSON.stringify({
|
|
icons: [
|
|
{ src: pngs.at(1), type: "image/png", sizes: "192x192" },
|
|
{ src: pngs.at(2), type: "image/png", sizes: "512x512" },
|
|
],
|
|
}),
|
|
),
|
|
);
|
|
return (
|
|
<>
|
|
<link rel="icon" href={ico} sizes="32x32" />
|
|
<link rel="apple-touch-icon" href={pngs.at(0)} />
|
|
{svg && <link rel="icon" href={svg} type="image/svg+xml" />}
|
|
<link rel="manifest" href={manifest} />
|
|
</>
|
|
);
|
|
};
|
|
|
|
const OgImage = async ({ media }: { media?: S3Object | null }) => {
|
|
if (!media?.key) return null;
|
|
const options = { resizes: [1200], format: ["image/png" as const] };
|
|
const sources = await sharp.createResponsiveImage([media], options);
|
|
const content = sources.flatMap((v) => v.sources.at(-1)?.srcSet ?? []).at(0);
|
|
return <meta property="og:image" content={content} />;
|
|
};
|
|
|
|
const joinArr = <T,>(arr: T[]): string => arr.filter((v) => v).join(" — ");
|
|
|
|
export default async ({
|
|
children,
|
|
title,
|
|
description,
|
|
className,
|
|
htmlClassName,
|
|
images,
|
|
fontsSources,
|
|
disableAnalytics,
|
|
}: {
|
|
title?: string;
|
|
className?: string;
|
|
htmlClassName?: string;
|
|
description?: string | null;
|
|
images?: S3Object[];
|
|
children?: React.ReactNode;
|
|
fontsSources: { href: string }[];
|
|
disableAnalytics?: boolean;
|
|
}) => {
|
|
const ctx = context<Context>();
|
|
const site = await database
|
|
.selectFrom("sites")
|
|
.selectAll()
|
|
.executeTakeFirstOrThrow();
|
|
const ogTitle = joinArr([title, site.title, site.subtitle]);
|
|
const ogDescription = joinArr([
|
|
description,
|
|
convertToPlain(site.description_html ?? ""),
|
|
]);
|
|
const siteUrl = site.url
|
|
? site.url.startsWith("http") ? site.url : `https://${site.url}`
|
|
: null;
|
|
const canonical = siteUrl ? new URL(ctx.url.pathname, siteUrl).href : null;
|
|
const locale = "fr_FR";
|
|
const hmr = getHmrScriptPath();
|
|
|
|
return (
|
|
<html lang={locale.split("_").at(0)} className={htmlClassName}>
|
|
<head>
|
|
<title>{ogTitle}</title>
|
|
<meta name="description" content={ogDescription} />
|
|
{fontsSources
|
|
.map((f) => ({ rel: "preload", type: "font/woff2", ...f }))
|
|
.map((f) => <link key={f.href} {...f} as="font" crossOrigin="" />)}
|
|
<link rel="preconnect" href={getS3Uri("").href} />
|
|
<Favicons
|
|
media={(site.logo_image_json || site.meta_image_json)?.at(0)}
|
|
/>
|
|
{canonical && <link rel="canonical" href={canonical} />}
|
|
{canonical && <meta property="og:url" content={canonical} />}
|
|
<OgImage media={(images || site.meta_image_json)?.at(0)} />
|
|
<meta property="og:locale" content={locale} />
|
|
<meta property="og:site_name" content={site.title} />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:title" content={ogTitle} />
|
|
<meta
|
|
name="author"
|
|
content="Bureau Double(Juliette Lépineau; Elias Rhouzlane)"
|
|
/>
|
|
<link rel="stylesheet" href={tailwind.pathname} />
|
|
{!disableAnalytics && <script src="/analytics/client.js" defer async />}
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1, maximum-scale=5, viewport-fit=cover"
|
|
/>
|
|
<meta charSet="utf-8" />
|
|
<meta httpEquiv="content-type" content="text/html; charset=utf-8" />
|
|
{false && <script src={hmr} defer async />}
|
|
</head>
|
|
<body className={className}>
|
|
<div
|
|
className="fixed top-0 h-1 after:block after:h-full after:bg-primary after:w-[calc(var(--value,_0)_*_1%)] w-full pointer-events-none z-50 after:shadow-sm"
|
|
id="rsc-fetch-value"
|
|
/>
|
|
{children}
|
|
</body>
|
|
</html>
|
|
);
|
|
};
|