feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
116
apps/web/src/lib/metadata.ts
Normal file
116
apps/web/src/lib/metadata.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { isKey } from "@turbostarter/i18n";
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
|
||||
import { appConfig } from "~/config/app";
|
||||
|
||||
import type { TranslationKey } from "@turbostarter/i18n";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
type OpenGraphType =
|
||||
| "article"
|
||||
| "book"
|
||||
| "music.song"
|
||||
| "music.album"
|
||||
| "music.playlist"
|
||||
| "music.radio_station"
|
||||
| "profile"
|
||||
| "website"
|
||||
| "video.tv_show"
|
||||
| "video.other"
|
||||
| "video.movie"
|
||||
| "video.episode";
|
||||
|
||||
interface SeoProps {
|
||||
readonly title?: TranslationKey | (string & Record<never, never>);
|
||||
readonly description?: TranslationKey | (string & Record<never, never>);
|
||||
readonly image?: string;
|
||||
readonly url?: string;
|
||||
readonly canonical?: string;
|
||||
readonly type?: OpenGraphType;
|
||||
readonly images?: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
alt?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const SITE_NAME_SEPARATOR = " | ";
|
||||
export const SITE_NAME_TEMPLATE = `%s${SITE_NAME_SEPARATOR}${appConfig.name}`;
|
||||
|
||||
const DEFAULT_IMAGE = {
|
||||
url: `${appConfig.url}/opengraph-image.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: appConfig.name,
|
||||
};
|
||||
|
||||
export const getMetadata =
|
||||
(
|
||||
{
|
||||
title,
|
||||
description = "common:product.description",
|
||||
url,
|
||||
canonical,
|
||||
images = [DEFAULT_IMAGE],
|
||||
type = "website",
|
||||
} = {} as SeoProps,
|
||||
) =>
|
||||
async ({
|
||||
params,
|
||||
}: {
|
||||
params?: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> => {
|
||||
const { t, i18n } = await getTranslation({
|
||||
locale: (await params)?.locale,
|
||||
});
|
||||
|
||||
const common = {
|
||||
...(title && {
|
||||
title: isKey(title, i18n) ? (t(title) as string) : title,
|
||||
}),
|
||||
description: isKey(description, i18n)
|
||||
? (t(description) as string)
|
||||
: description,
|
||||
};
|
||||
|
||||
return {
|
||||
...common,
|
||||
openGraph: {
|
||||
...common,
|
||||
url: url ?? canonical ?? appConfig.url,
|
||||
siteName: appConfig.name,
|
||||
type,
|
||||
images,
|
||||
},
|
||||
...{
|
||||
...(canonical && {
|
||||
alternates: {
|
||||
canonical,
|
||||
},
|
||||
}),
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image" as const,
|
||||
images,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_METADATA: Metadata = {
|
||||
...(await getMetadata()({
|
||||
params: Promise.resolve({ locale: appConfig.locale }),
|
||||
})),
|
||||
title: {
|
||||
template: SITE_NAME_TEMPLATE,
|
||||
default: appConfig.name,
|
||||
},
|
||||
metadataBase: appConfig.url ? new URL(appConfig.url) : null,
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEWPORT: Viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#030712" },
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user