import { describe, expect, it } from 'vitest'; import { pageMetadata } from '#web-runtime/lib/page-metadata'; import type { PageDocument, PageRenderData, SiteSettings } from '#web-runtime/types'; const settings: SiteSettings = { locale: 'en', header: { menus: [], ctaItems: [] }, footer: { copyright: '', menus: [] }, legal: { privacyPath: '', termsPath: '' } }; function page(overrides: Partial = {}): PageDocument { return { id: '7', status: 'published', locale: 'en', path: 'about', groupKey: 'about', layout: 'landing', title: 'About us', seo: { title: 'About us — Crux', description: 'Who we are' }, breadcrumbs: [], blocks: [], publishedAt: null, updatedAt: null, ...overrides }; } function renderData(overrides: Partial = {}): PageRenderData { return { page: page(), settings, languageAlternatives: [], ...overrides }; } /** The site serves locale-prefixed paths; metadataBase turns these into absolute URLs. */ const publicPath = (locale: string, path: string) => `/${locale}${path ? `/${path}` : ''}`; describe('title and description', () => { it('prefers the editor SEO fields', () => { const metadata = pageMetadata(renderData(), 'en', publicPath); expect(metadata.title).toBe('About us — Crux'); expect(metadata.description).toBe('Who we are'); }); it('falls back to the page title, and omits an empty description entirely', () => { const data = renderData({ page: page({ seo: { title: '', description: '' } }) }); const metadata = pageMetadata(data, 'en', publicPath); expect(metadata.title).toBe('About us'); expect(metadata.description).toBeUndefined(); }); }); describe('canonical and hreflang', () => { it('always sets a canonical for the page itself', () => { expect(pageMetadata(renderData(), 'en', publicPath).alternates?.canonical).toBe('/en/about'); }); it('emits no language map for a page with no translations', () => { const data = renderData({ languageAlternatives: [{ locale: 'en', path: 'about' }] }); expect(pageMetadata(data, 'en', publicPath).alternates?.languages).toBeUndefined(); }); it('lists every locale including this one, plus x-default', () => { const data = renderData({ languageAlternatives: [ { locale: 'de', path: 'ueber-uns' }, { locale: 'en', path: 'about' }, { locale: 'uk', path: 'pro-nas' } ] }); expect(pageMetadata(data, 'en', publicPath).alternates?.languages).toEqual({ 'de': '/de/ueber-uns', 'en': '/en/about', 'uk': '/uk/pro-nas', 'x-default': '/en/about' }); }); it('points x-default at this page when the default locale has no translation', () => { const data = renderData({ page: page({ locale: 'uk', path: 'pro-nas' }), languageAlternatives: [ { locale: 'uk', path: 'pro-nas' }, { locale: 'de', path: 'ueber-uns' } ] }); expect(pageMetadata(data, 'en', publicPath).alternates?.languages).toMatchObject({ 'x-default': '/uk/pro-nas' }); }); }); describe('share cards', () => { const media = { url: 'https://cdn.example.com/hero.jpg', alt: 'Team photo', width: 800, height: 525 }; it('uses the hero image, with the dimensions crawlers lay out against', () => { const data = renderData({ page: page({ hero: { type: 'middle', ctaItems: [], media } }) }); const metadata = pageMetadata(data, 'en', publicPath); expect(metadata.openGraph?.images).toEqual([ { url: media.url, alt: 'Team photo', width: 800, height: 525 } ]); expect(metadata.twitter).toMatchObject({ card: 'summary_large_image' }); }); it('falls back to the page title when the image has no alt text', () => { const data = renderData({ page: page({ hero: { type: 'middle', ctaItems: [], media: { ...media, alt: '' } } }) }); expect(pageMetadata(data, 'en', publicPath).openGraph?.images).toMatchObject([ { alt: 'About us' } ]); }); it('omits dimensions the backend did not provide rather than guessing', () => { const data = renderData({ page: page({ hero: { type: 'middle', ctaItems: [], media: { url: media.url, alt: 'x' } } }) }); expect(pageMetadata(data, 'en', publicPath).openGraph?.images).toEqual([ { url: media.url, alt: 'x' } ]); }); it('drops to a small card when the page has no hero image', () => { const metadata = pageMetadata(renderData(), 'en', publicPath); expect(metadata.openGraph?.images).toBeUndefined(); expect(metadata.twitter).toMatchObject({ card: 'summary' }); }); }); describe('open graph type', () => { it('marks an editorial page as an article and everything else as a page', () => { const article = renderData({ page: page({ layout: 'blog-item' }) }); const landing = renderData({ page: page({ layout: 'landing' }) }); expect(pageMetadata(article, 'en', publicPath).openGraph).toMatchObject({ type: 'article' }); expect(pageMetadata(landing, 'en', publicPath).openGraph).toMatchObject({ type: 'website' }); }); it('carries the page locale and its own url', () => { expect(pageMetadata(renderData(), 'en', publicPath).openGraph).toMatchObject({ locale: 'en', url: '/en/about' }); }); });