chore: heygermany customer project
This commit is contained in:
225
apps/website/lib/seo.ts
Normal file
225
apps/website/lib/seo.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import type { SupportedLocale } from '@heygermany/sdk/config'
|
||||
import { DEFAULT_LOCALE, LOCALES } from '@/config'
|
||||
import { mTextIn } from '@/i18n/messages'
|
||||
|
||||
type MessageKey = string
|
||||
|
||||
export const SITE_NAME = 'HeyGermany'
|
||||
export const BRAND_ALT_NAME = 'Hey Germany'
|
||||
export const CONTACT_EMAIL = 'info@hey-germany.com'
|
||||
const viteEnv = (import.meta as ImportMeta & {
|
||||
env?: Record<string, string | undefined>
|
||||
}).env
|
||||
|
||||
export const SITE_URL =
|
||||
viteEnv?.VITE_SITE_URL || process.env.VITE_SITE_URL || 'https://hey-germany.com'
|
||||
export const DEFAULT_SITE_TITLE = `${SITE_NAME} | Nursing Career and Qualification Check in Germany`
|
||||
export const DEFAULT_SITE_DESCRIPTION =
|
||||
'HeyGermany helps internationally qualified nurses check their qualifications and take the next step toward a nursing career in Germany.'
|
||||
export const DEFAULT_OG_IMAGE = '/heygermany-logo.png'
|
||||
export const NO_INDEX_METADATA = {
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
nocache: true,
|
||||
googleBot: {
|
||||
index: false,
|
||||
follow: false,
|
||||
noimageindex: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type CreatePageMetadataInput = {
|
||||
locale: string
|
||||
pathname?: string
|
||||
title: string
|
||||
description: string
|
||||
imagePath?: string
|
||||
index?: boolean
|
||||
availableLocales?: readonly SupportedLocale[]
|
||||
canonicalLocale?: SupportedLocale
|
||||
}
|
||||
|
||||
function isSupportedLocale(locale: string): locale is SupportedLocale {
|
||||
return LOCALES.includes(locale as SupportedLocale)
|
||||
}
|
||||
|
||||
function normalizePath(pathname: string): string {
|
||||
if (!pathname || pathname === '/') {
|
||||
return '/'
|
||||
}
|
||||
|
||||
return pathname.startsWith('/') ? pathname : `/${pathname}`
|
||||
}
|
||||
|
||||
function buildRobots(index: boolean) {
|
||||
return index
|
||||
? {
|
||||
index: true,
|
||||
follow: true,
|
||||
}
|
||||
: NO_INDEX_METADATA.robots
|
||||
}
|
||||
|
||||
export function normalizeLocale(locale: string): SupportedLocale {
|
||||
return isSupportedLocale(locale) ? locale : DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
export function getAbsoluteUrl(pathname: string): string {
|
||||
return new URL(pathname, SITE_URL).toString()
|
||||
}
|
||||
|
||||
export function getLocalizedPath(locale: string, pathname = '/'): string {
|
||||
const normalizedLocale = normalizeLocale(locale)
|
||||
const normalizedPath = normalizePath(pathname)
|
||||
|
||||
return normalizedPath === '/'
|
||||
? `/${normalizedLocale}`
|
||||
: `/${normalizedLocale}${normalizedPath}`
|
||||
}
|
||||
|
||||
export function withSiteName(title: string): string {
|
||||
return title.includes(SITE_NAME) ? title : `${title} | ${SITE_NAME}`
|
||||
}
|
||||
|
||||
export function getMessage(locale: string, key: MessageKey): string {
|
||||
return mTextIn(normalizeLocale(locale), key)
|
||||
}
|
||||
|
||||
function buildLanguageAlternates(
|
||||
pathname: string,
|
||||
locales: readonly SupportedLocale[]
|
||||
): Record<string, string> | undefined {
|
||||
if (locales.length < 2) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const alternates = Object.fromEntries(
|
||||
locales.map((locale) => [locale, getAbsoluteUrl(getLocalizedPath(locale, pathname))])
|
||||
) as Record<string, string>
|
||||
|
||||
alternates['x-default'] = getAbsoluteUrl(getLocalizedPath(DEFAULT_LOCALE, pathname))
|
||||
|
||||
return alternates
|
||||
}
|
||||
|
||||
export function createPageMetadata({
|
||||
locale,
|
||||
pathname = '/',
|
||||
title,
|
||||
description,
|
||||
imagePath = DEFAULT_OG_IMAGE,
|
||||
index = true,
|
||||
availableLocales = LOCALES,
|
||||
canonicalLocale,
|
||||
}: CreatePageMetadataInput) {
|
||||
const resolvedLocale = normalizeLocale(locale)
|
||||
const resolvedCanonicalLocale = canonicalLocale ?? resolvedLocale
|
||||
const canonicalPath = getLocalizedPath(resolvedCanonicalLocale, pathname)
|
||||
const resolvedTitle = withSiteName(title)
|
||||
const imageUrl = getAbsoluteUrl(imagePath)
|
||||
const languages = buildLanguageAlternates(pathname, availableLocales)
|
||||
|
||||
return {
|
||||
title: resolvedTitle,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: getAbsoluteUrl(canonicalPath),
|
||||
...(languages ? { languages } : {}),
|
||||
},
|
||||
openGraph: {
|
||||
title: resolvedTitle,
|
||||
description,
|
||||
url: getAbsoluteUrl(canonicalPath),
|
||||
siteName: SITE_NAME,
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: imageUrl,
|
||||
alt: resolvedTitle,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: resolvedTitle,
|
||||
description,
|
||||
images: [imageUrl],
|
||||
},
|
||||
robots: buildRobots(index),
|
||||
}
|
||||
}
|
||||
|
||||
export function createHomepageMetadata(locale: string) {
|
||||
const resolvedLocale = normalizeLocale(locale)
|
||||
|
||||
const homepageMetadata: Partial<
|
||||
Record<SupportedLocale, { title: string; description: string }>
|
||||
> = {
|
||||
en: {
|
||||
title: DEFAULT_SITE_TITLE,
|
||||
description: DEFAULT_SITE_DESCRIPTION,
|
||||
},
|
||||
de: {
|
||||
title: `${SITE_NAME} | Pflegekarriere und Qualifikationscheck in Deutschland`,
|
||||
description:
|
||||
'HeyGermany unterstützt internationale Pflegefachkräfte dabei, ihre Qualifikation für Deutschland zu prüfen und den nächsten Schritt in ihre Pflegekarriere zu gehen.',
|
||||
},
|
||||
}
|
||||
|
||||
const localizedBrandMetadata = homepageMetadata[resolvedLocale]
|
||||
|
||||
return createPageMetadata({
|
||||
locale: resolvedLocale,
|
||||
title: localizedBrandMetadata?.title ?? `${SITE_NAME} | ${getMessage(locale, 'website.hero.title')}`,
|
||||
description: localizedBrandMetadata?.description ?? getMessage(locale, 'website.hero.subtitle'),
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrganizationStructuredData() {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
'@id': `${SITE_URL}#organization`,
|
||||
name: SITE_NAME,
|
||||
alternateName: BRAND_ALT_NAME,
|
||||
url: SITE_URL,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: getAbsoluteUrl(DEFAULT_OG_IMAGE),
|
||||
},
|
||||
email: CONTACT_EMAIL,
|
||||
description: DEFAULT_SITE_DESCRIPTION,
|
||||
contactPoint: [
|
||||
{
|
||||
'@type': 'ContactPoint',
|
||||
contactType: 'customer support',
|
||||
email: CONTACT_EMAIL,
|
||||
url: SITE_URL,
|
||||
availableLanguage: LOCALES,
|
||||
},
|
||||
],
|
||||
brand: {
|
||||
'@type': 'Brand',
|
||||
name: SITE_NAME,
|
||||
alternateName: BRAND_ALT_NAME,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getWebsiteStructuredData() {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
'@id': `${SITE_URL}#website`,
|
||||
url: SITE_URL,
|
||||
name: SITE_NAME,
|
||||
alternateName: BRAND_ALT_NAME,
|
||||
description: DEFAULT_SITE_DESCRIPTION,
|
||||
publisher: {
|
||||
'@id': `${SITE_URL}#organization`,
|
||||
},
|
||||
inLanguage: LOCALES,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user