chore: heygermany customer project
This commit is contained in:
102
apps/website/lib/auth.ts
Normal file
102
apps/website/lib/auth.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
type AuthUser = {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
role?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
const getApiBase = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return ''
|
||||
}
|
||||
|
||||
const viteEnv = (import.meta as ImportMeta & {
|
||||
env?: Record<string, string | undefined>
|
||||
}).env
|
||||
|
||||
return viteEnv?.VITE_API_BASE_URL || process.env.VITE_API_BASE_URL || process.env.API_URL || ''
|
||||
}
|
||||
|
||||
const getErrorMessage = async (response: Response) => {
|
||||
const text = await response.text()
|
||||
|
||||
if (!text) {
|
||||
return `Request failed with status ${response.status}`
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text) as { message?: string; error?: string }
|
||||
return data.message || data.error || text
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
const authFetch = async <T>(path: string, init?: RequestInit): Promise<T> => {
|
||||
const headers = new Headers(init?.headers)
|
||||
const hasBody = init?.body !== undefined && init?.body !== null
|
||||
|
||||
if (hasBody && !headers.has('content-type')) {
|
||||
headers.set('content-type', 'application/json')
|
||||
}
|
||||
|
||||
const response = await fetch(`${getApiBase()}${path}`, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await getErrorMessage(response))
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
return await response.json() as T
|
||||
}
|
||||
|
||||
export const signInWithPassword = async (email: string, password: string) => {
|
||||
return await authFetch<{
|
||||
redirect: boolean
|
||||
token: string
|
||||
url?: string
|
||||
user: AuthUser
|
||||
}>('/api/auth/sign-in/email', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const requestMagicLink = async (email: string) => {
|
||||
return await authFetch<{ status: boolean }>('/api/auth/sign-in/magic-link', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
}
|
||||
|
||||
export const verifyMagicLink = async (token: string) => {
|
||||
return await authFetch<{
|
||||
token: string
|
||||
user: AuthUser
|
||||
session: {
|
||||
id: string
|
||||
userId: string
|
||||
expiresAt: string
|
||||
}
|
||||
}>(`/api/auth/magic-link/verify?token=${encodeURIComponent(token)}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export const signOut = async () => {
|
||||
return await authFetch('/api/auth/sign-out', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
}
|
||||
3
apps/website/lib/health.ts
Normal file
3
apps/website/lib/health.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function getHealthResponse() {
|
||||
return new Response('OK')
|
||||
}
|
||||
261
apps/website/lib/request-routing.ts
Normal file
261
apps/website/lib/request-routing.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { match } from '@formatjs/intl-localematcher'
|
||||
import Negotiator from 'negotiator'
|
||||
import type { UserSession } from '@heygermany/functions/src/application-types.d.js'
|
||||
import type { DB, UserRole, UserType } from '@heygermany/sdk'
|
||||
import { UserTypes } from '@heygermany/sdk'
|
||||
import type { SupportedLocale } from '@heygermany/sdk/config'
|
||||
import { LOCALES, DEFAULT_LOCALE } from '@/config'
|
||||
|
||||
type RequestSnapshot = {
|
||||
url: string
|
||||
pathname: string
|
||||
search: string
|
||||
headers: Headers
|
||||
cookies: Map<string, string>
|
||||
}
|
||||
|
||||
type RequestRoutingResult =
|
||||
| {
|
||||
type: 'redirect'
|
||||
location: string
|
||||
}
|
||||
| {
|
||||
type: 'continue'
|
||||
locale: string
|
||||
}
|
||||
|
||||
const LOCALE_EXEMPT_PREFIXES = ['/pilot']
|
||||
const LOCALE_EXEMPT_LOCALES: Record<string, SupportedLocale> = {
|
||||
'/pilot': 'de',
|
||||
}
|
||||
|
||||
const getApiBase = (request: RequestSnapshot) => {
|
||||
return process.env.VITE_API_BASE_URL || process.env.API_URL || new URL(request.url).origin
|
||||
}
|
||||
|
||||
function shouldBypassRouting(pathname: string): boolean {
|
||||
return (
|
||||
pathname.startsWith('/api/') ||
|
||||
pathname === '/api' ||
|
||||
pathname.startsWith('/_build/') ||
|
||||
pathname === '/_build' ||
|
||||
pathname.startsWith('/_serverFn/') ||
|
||||
pathname === '/_serverFn' ||
|
||||
pathname.startsWith('/@') ||
|
||||
pathname.startsWith('/src/') ||
|
||||
pathname.startsWith('/node_modules/') ||
|
||||
/\.[a-z0-9]+$/i.test(pathname)
|
||||
)
|
||||
}
|
||||
|
||||
const getCookieHeader = (cookies: Map<string, string>) => {
|
||||
return Array.from(cookies.entries())
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
const getBetterAuthSession = async (
|
||||
request: RequestSnapshot
|
||||
): Promise<UserSession | null> => {
|
||||
const response = await fetch(`${getApiBase(request)}/api/auth/get-session`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
cookie: getCookieHeader(request.cookies),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
user?: {
|
||||
id: string
|
||||
name: string
|
||||
role: UserRole
|
||||
type: UserType
|
||||
} | null
|
||||
} | null
|
||||
|
||||
if (!data?.user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
userId: data.user.id,
|
||||
name: data.user.name,
|
||||
role: data.user.role,
|
||||
type: data.user.type,
|
||||
}
|
||||
}
|
||||
|
||||
function getLocale(request: RequestSnapshot): string {
|
||||
try {
|
||||
const cookieLocale = request.cookies.get('NEXT_LOCALE')
|
||||
if (cookieLocale && LOCALES.includes(cookieLocale as SupportedLocale)) {
|
||||
return cookieLocale
|
||||
}
|
||||
|
||||
const headers = Object.fromEntries(request.headers.entries()) as Negotiator.Headers
|
||||
const languages = new Negotiator({ headers }).languages()
|
||||
const validLanguages = languages.filter((lang) => {
|
||||
try {
|
||||
Intl.getCanonicalLocales(lang)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (validLanguages.length === 0) {
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
return match(validLanguages, LOCALES, DEFAULT_LOCALE)
|
||||
} catch (error) {
|
||||
console.error('[Middleware] Error in locale detection:', error)
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
}
|
||||
|
||||
function hasLocale(pathname: string): boolean {
|
||||
return LOCALES.some((locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`)
|
||||
}
|
||||
|
||||
function isLocaleExemptPath(pathname: string): boolean {
|
||||
return LOCALE_EXEMPT_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))
|
||||
}
|
||||
|
||||
function getLocaleExemptPathLocale(pathname: string): SupportedLocale | null {
|
||||
for (const [prefix, locale] of Object.entries(LOCALE_EXEMPT_LOCALES)) {
|
||||
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
||||
return locale
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function getLocaleFromPath(pathname: string): string | null {
|
||||
for (const locale of LOCALES) {
|
||||
if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) {
|
||||
return locale
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function removeLocalePrefix(pathname: string): string {
|
||||
const locale = getLocaleFromPath(pathname)
|
||||
if (locale) {
|
||||
return pathname.slice(`/${locale}`.length) || '/'
|
||||
}
|
||||
|
||||
return pathname
|
||||
}
|
||||
|
||||
function requiresAuth(pathname: string): { required: boolean; type?: DB.UserType } {
|
||||
const pathWithoutLocale = removeLocalePrefix(pathname)
|
||||
|
||||
if (pathWithoutLocale.includes('/auth/login')) {
|
||||
return { required: false }
|
||||
}
|
||||
|
||||
if (pathWithoutLocale.startsWith('/backoffice')) {
|
||||
return { required: true, type: UserTypes.BACKOFFICE }
|
||||
}
|
||||
|
||||
if (pathWithoutLocale.startsWith('/company')) {
|
||||
return { required: true, type: UserTypes.COMPANY }
|
||||
}
|
||||
|
||||
if (pathWithoutLocale.startsWith('/jobs/application')) {
|
||||
return { required: true, type: UserTypes.CANDIDATE }
|
||||
}
|
||||
|
||||
return { required: false }
|
||||
}
|
||||
|
||||
const toAbsoluteUrl = (request: RequestSnapshot, location: string) => {
|
||||
return new URL(location, request.url).toString()
|
||||
}
|
||||
|
||||
export const getResolvedLocale = (pathname: string) => {
|
||||
return getLocaleFromPath(pathname) || getLocaleExemptPathLocale(pathname) || DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
export async function resolveRequestRouting(
|
||||
request: RequestSnapshot
|
||||
): Promise<RequestRoutingResult> {
|
||||
if (shouldBypassRouting(request.pathname)) {
|
||||
return {
|
||||
type: 'continue',
|
||||
locale: getResolvedLocale(request.pathname),
|
||||
}
|
||||
}
|
||||
|
||||
const fullUrl = request.pathname + request.search
|
||||
|
||||
if (!hasLocale(request.pathname) && !isLocaleExemptPath(request.pathname)) {
|
||||
const locale = getLocale(request)
|
||||
return {
|
||||
type: 'redirect',
|
||||
location: toAbsoluteUrl(request, `/${locale}${request.pathname}${request.search}`),
|
||||
}
|
||||
}
|
||||
|
||||
const userSession = await getBetterAuthSession(request)
|
||||
const authCheck = requiresAuth(request.pathname)
|
||||
|
||||
if (authCheck.required) {
|
||||
const currentLocale = getLocaleFromPath(request.pathname) || DEFAULT_LOCALE
|
||||
|
||||
if (userSession?.type !== UserTypes.BACKOFFICE) {
|
||||
if (!userSession || userSession.type !== authCheck.type) {
|
||||
let loginPath = ''
|
||||
if (authCheck.type === UserTypes.BACKOFFICE) {
|
||||
loginPath = `/${currentLocale}/backoffice/auth/login`
|
||||
} else if (authCheck.type === UserTypes.COMPANY) {
|
||||
loginPath = `/${currentLocale}/company/auth/login`
|
||||
} else if (authCheck.type === UserTypes.CANDIDATE) {
|
||||
loginPath = `/${currentLocale}/jobs/apply`
|
||||
}
|
||||
|
||||
const returnUrl = encodeURIComponent(fullUrl)
|
||||
return {
|
||||
type: 'redirect',
|
||||
location: toAbsoluteUrl(request, `${loginPath}?return=${returnUrl}`),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userSession) {
|
||||
const pathWithoutLocale = removeLocalePrefix(request.pathname)
|
||||
const currentLocale = getLocaleFromPath(request.pathname) || DEFAULT_LOCALE
|
||||
|
||||
if (pathWithoutLocale.includes('/auth/login')) {
|
||||
let dashboardPath = ''
|
||||
if (userSession.type === UserTypes.BACKOFFICE) {
|
||||
dashboardPath = `/${currentLocale}/backoffice/candidates`
|
||||
} else if (userSession.type === UserTypes.COMPANY) {
|
||||
dashboardPath = `/${currentLocale}/company/candidates`
|
||||
} else if (userSession.type === UserTypes.CANDIDATE) {
|
||||
dashboardPath = `/${currentLocale}/jobs/application`
|
||||
}
|
||||
|
||||
if (dashboardPath) {
|
||||
return {
|
||||
type: 'redirect',
|
||||
location: toAbsoluteUrl(request, dashboardPath),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'continue',
|
||||
locale: getResolvedLocale(request.pathname),
|
||||
}
|
||||
}
|
||||
15
apps/website/lib/robots.ts
Normal file
15
apps/website/lib/robots.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { SITE_URL } from '@/lib/seo'
|
||||
|
||||
export function getRobotsConfig() {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/api/'],
|
||||
},
|
||||
],
|
||||
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||
host: SITE_URL,
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
94
apps/website/lib/sitemap.ts
Normal file
94
apps/website/lib/sitemap.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { DEFAULT_LOCALE, LOCALES } from '../config'
|
||||
|
||||
const PUBLIC_LOCALIZED_PATHS = ['/', '/about', '/jobs/apply'] as const
|
||||
const LEGAL_PATHS = [
|
||||
'/legal/imprint',
|
||||
'/legal/privacy',
|
||||
'/legal/terms-and-conditions',
|
||||
] as const
|
||||
|
||||
type SitemapPage = {
|
||||
path: string
|
||||
sitemap: {
|
||||
lastmod: string
|
||||
changefreq: 'weekly' | 'monthly' | 'yearly'
|
||||
priority: number
|
||||
alternateRefs?: Array<{
|
||||
href: string
|
||||
hreflang: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePath(pathname: string): string {
|
||||
if (!pathname || pathname === '/') {
|
||||
return '/'
|
||||
}
|
||||
|
||||
return pathname.startsWith('/') ? pathname : `/${pathname}`
|
||||
}
|
||||
|
||||
function getLocalizedPath(locale: string, pathname = '/'): string {
|
||||
const normalizedPath = normalizePath(pathname)
|
||||
|
||||
return normalizedPath === '/'
|
||||
? `/${locale}`
|
||||
: `/${locale}${normalizedPath}`
|
||||
}
|
||||
|
||||
const getLocalizedChangeFrequency = (
|
||||
pathname: (typeof PUBLIC_LOCALIZED_PATHS)[number]
|
||||
): SitemapPage['sitemap']['changefreq'] => {
|
||||
return pathname === '/' ? 'weekly' : 'monthly'
|
||||
}
|
||||
|
||||
function getAlternateRefs(pathname: (typeof PUBLIC_LOCALIZED_PATHS)[number]) {
|
||||
return [
|
||||
...LOCALES.map((locale) => ({
|
||||
href: getLocalizedPath(locale, pathname),
|
||||
hreflang: locale,
|
||||
})),
|
||||
{
|
||||
href: getLocalizedPath(DEFAULT_LOCALE, pathname),
|
||||
hreflang: 'x-default',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function getSitemapPages(lastModified = new Date()): SitemapPage[] {
|
||||
const lastmod = lastModified.toISOString().split('T')[0]!
|
||||
|
||||
const localizedPages: SitemapPage[] = LOCALES.flatMap((locale) =>
|
||||
PUBLIC_LOCALIZED_PATHS.map((pathname) => ({
|
||||
path: getLocalizedPath(locale, pathname),
|
||||
sitemap: {
|
||||
lastmod,
|
||||
changefreq: getLocalizedChangeFrequency(pathname),
|
||||
priority: pathname === '/' ? 1 : 0.8,
|
||||
alternateRefs: getAlternateRefs(pathname),
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
const legalPages: SitemapPage[] = LEGAL_PATHS.map((pathname) => ({
|
||||
path: getLocalizedPath(DEFAULT_LOCALE, pathname),
|
||||
sitemap: {
|
||||
lastmod,
|
||||
changefreq: 'yearly',
|
||||
priority: 0.3,
|
||||
},
|
||||
}))
|
||||
|
||||
return [
|
||||
...localizedPages,
|
||||
...legalPages,
|
||||
{
|
||||
path: '/pilot',
|
||||
sitemap: {
|
||||
lastmod,
|
||||
changefreq: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user