Add initial project files
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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> = {}): 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> = {}): 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'
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user