Files
Portfolio/e2e/seo.e2e.ts
Antonio Ledebuhr a9ad7eb1a7 Rewrite the constraint specs and docs for the service-led information architecture.
Assertions now follow the eighteen prerenderable paths, the gist dialog, and the six-case evidence set.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 00:41:24 +02:00

141 lines
5.9 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { SITE_CONTENT_DATA } from '../src/app/core/content/site-content';
import { prerenderablePaths } from '../src/app/core/routing/route-paths';
import { expectHead, jsonLdGraph, pagePath } from './helpers';
async function ensurePrimaryNavOpen(page: Page): Promise<void> {
const toggle = page.locator('.nav-toggle');
if ((await toggle.isVisible()) && (await toggle.getAttribute('aria-expanded')) === 'false') {
await toggle.click();
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
}
}
test.describe('server-rendered metadata', () => {
test('German and English Home include the full head contract', async ({ request, page }) => {
await expectHead(request, 'home', 'de');
await expectHead(request, 'home', 'en');
await page.goto('/');
const graph = (await jsonLdGraph(page)) as { '@graph': Array<{ '@type': string }> };
const types = graph['@graph'].map((node) => node['@type']);
expect(types).toContain('Person');
expect(types).toContain('ProfessionalService');
await page.goto('/en');
const english = (await jsonLdGraph(page)) as { '@graph': Array<{ '@type': string }> };
expect(english['@graph'].map((node) => node['@type'])).toEqual(
expect.arrayContaining(['Person', 'ProfessionalService']),
);
});
test('projects, services, contact, legal and 404 keep locale metadata', async ({ request }) => {
for (const locale of ['de', 'en'] as const) {
await expectHead(request, 'projects', locale);
await expectHead(request, 'services', locale);
await expectHead(request, 'contact', locale);
await expectHead(request, 'imprint', locale);
await expectHead(request, 'notFound', locale);
}
});
test('pitch has full head metadata and stays out of the primary nav', async ({
request,
page,
}) => {
await expectHead(request, 'pitch', 'de');
await expectHead(request, 'pitch', 'en');
const sitemap = await request.get('/sitemap.xml');
const xml = await sitemap.text();
expect(xml).toContain('https://antoniolede.de/pitch');
expect(xml).toContain('https://antoniolede.de/en/pitch');
await page.goto('/');
await ensurePrimaryNavOpen(page);
const nav = page.locator('.primary-nav');
await expect(nav.locator(`a[href="${pagePath('pitch', 'de')}"]`)).toHaveCount(0);
await expect(nav.locator(`a[href="${pagePath('home', 'de')}"]`)).toHaveCount(1);
});
test('every prerenderable path returns 200', async ({ request }) => {
const paths = prerenderablePaths();
expect(paths).toHaveLength(18);
for (const path of paths) {
const response = await request.get(path);
expect(response.status(), path).toBe(200);
}
});
test('unmatched URLs return 404 and a real route stays 200', async ({ request }) => {
const home = await request.get('/');
expect(home.status(), 'GET /').toBe(200);
await expectHead(request, 'notFound', 'de');
await expectHead(request, 'notFound', 'en');
});
test('client navigation does not duplicate head tags', async ({ page }) => {
await page.goto('/');
await ensurePrimaryNavOpen(page);
await page.locator('.language-switch').first().waitFor();
const assertUnique = async (canonical: string, description: string) => {
await expect(page.locator('link[rel="canonical"]')).toHaveCount(1);
await expect(page.locator('link[rel="alternate"][hreflang="de-DE"]')).toHaveCount(1);
await expect(page.locator('link[rel="alternate"][hreflang="en"]')).toHaveCount(1);
await expect(page.locator('link[rel="alternate"][hreflang="x-default"]')).toHaveCount(1);
await expect(page.locator('meta[name="description"]')).toHaveCount(1);
await expect(page.locator('meta[name="robots"]')).toHaveCount(1);
await expect(page.locator('script[type="application/ld+json"]')).toHaveCount(1);
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', canonical);
await expect(page.locator('meta[name="description"]')).toHaveAttribute(
'content',
description,
);
};
await ensurePrimaryNavOpen(page);
await page
.locator(`a[href="${pagePath('projects', 'de')}"]`)
.first()
.click();
await page.waitForURL('**/projekte');
await assertUnique(
'https://antoniolede.de/projekte',
SITE_CONTENT_DATA.de.pages.projects.description,
);
await ensurePrimaryNavOpen(page);
await page.locator(`.primary-nav a[href="${pagePath('services', 'de')}"]`).click();
await page.waitForURL('**/leistungen');
await expect(page.locator('link[rel="canonical"]')).toHaveCount(1);
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
'href',
'https://antoniolede.de/leistungen',
);
await expect(page.locator('script[type="application/ld+json"]')).toHaveCount(1);
});
test('legacy service URLs return HTTP 308 with the fragment Location', async ({ request }) => {
const redirects = [
['/leistungen/software', '/leistungen#software-data'],
['/leistungen/software/', '/leistungen#software-data'],
['/leistungen/hardware-netzwerk', '/leistungen#infrastructure-network'],
['/leistungen/cluster', '/leistungen#platform-operations'],
['/leistungen/ai-integration', '/leistungen#ai-workflows'],
['/en/services/software', '/en/services#software-data'],
['/en/services/hardware-network', '/en/services#infrastructure-network'],
['/en/services/clusters', '/en/services#platform-operations'],
['/en/services/ai-integration', '/en/services#ai-workflows'],
['/en/services/ai-integration/', '/en/services#ai-workflows'],
] as const;
for (const [from, to] of redirects) {
const response = await request.get(from, { maxRedirects: 0 });
expect(response.status(), from).toBe(308);
expect(response.headers().location, from).toBe(to);
}
});
});