TASKS-1823 WebManager module
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import type { Metadata } from 'next';
|
||||
import type { PageRenderData } from '#web-runtime/types';
|
||||
|
||||
/** Resolves a page's public URL path; injected so this module stays free of routing setup. */
|
||||
export type PublicPathResolver = (locale: string, path: string) => string;
|
||||
|
||||
/**
|
||||
* `x-default` tells search engines which version to serve when no language matches. The
|
||||
* site's default locale is the honest answer; if this page has no version in that locale,
|
||||
* the page itself is the next best thing.
|
||||
*/
|
||||
function xDefaultLocale(data: PageRenderData, defaultLocale: string): string {
|
||||
const hasDefault = data.languageAlternatives.some(
|
||||
(alternative) => alternative.locale === defaultLocale
|
||||
);
|
||||
|
||||
return hasDefault ? defaultLocale : data.page.locale;
|
||||
}
|
||||
|
||||
function languageAlternates(
|
||||
data: PageRenderData,
|
||||
defaultLocale: string,
|
||||
publicPath: PublicPathResolver
|
||||
): Record<string, string> | undefined {
|
||||
const hasTranslations = data.languageAlternatives.some(
|
||||
(alternative) => alternative.locale !== data.page.locale
|
||||
);
|
||||
|
||||
if (!hasTranslations) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const languages = Object.fromEntries(
|
||||
data.languageAlternatives.map((alternative) => [
|
||||
alternative.locale,
|
||||
publicPath(alternative.locale, alternative.path)
|
||||
])
|
||||
);
|
||||
const fallbackLocale = xDefaultLocale(data, defaultLocale);
|
||||
const fallbackPath = languages[fallbackLocale];
|
||||
|
||||
if (fallbackPath !== undefined) {
|
||||
languages['x-default'] = fallbackPath;
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* The hero image doubles as the share card. Width and height are included when the backend
|
||||
* knows them, because crawlers use them to pick a layout before fetching the file.
|
||||
*/
|
||||
function shareImages(data: PageRenderData): NonNullable<Metadata['openGraph']>['images'] {
|
||||
const media = data.page.hero?.media;
|
||||
|
||||
if (!media) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
url: media.url,
|
||||
alt: media.alt || data.page.title,
|
||||
...(media.width !== undefined ? { width: media.width } : {}),
|
||||
...(media.height !== undefined ? { height: media.height } : {})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a resolved public page. Kept separate from the route so it can be tested
|
||||
* without a Next request: everything it needs arrives as an argument.
|
||||
*/
|
||||
export function pageMetadata(
|
||||
data: PageRenderData,
|
||||
defaultLocale: string,
|
||||
publicPath: PublicPathResolver
|
||||
): Metadata {
|
||||
const title = data.page.seo.title || data.page.title;
|
||||
const description = data.page.seo.description || undefined;
|
||||
const canonical = publicPath(data.page.locale, data.page.path);
|
||||
const languages = languageAlternates(data, defaultLocale, publicPath);
|
||||
const images = shareImages(data);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical,
|
||||
...(languages ? { languages } : {})
|
||||
},
|
||||
openGraph: {
|
||||
// An editorial page is an article to a crawler; everything else is a plain page.
|
||||
type: data.page.layout === 'blog-item' ? 'article' : 'website',
|
||||
title,
|
||||
description,
|
||||
locale: data.page.locale,
|
||||
url: canonical,
|
||||
...(images ? { images } : {})
|
||||
},
|
||||
twitter: {
|
||||
card: images ? 'summary_large_image' : 'summary',
|
||||
title,
|
||||
description,
|
||||
...(images ? { images } : {})
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user