import assert from 'node:assert/strict' import test from 'node:test' import { SEO_STAGING_ROBOTS, seoAbsolute, seoAnswer, seoEnabled, seoHeadInput, seoRedirectTarget, seoResolvableUrl, seoRobots, seoVerificationName, seoXRobotsTag, } from '../app/utils/seo.ts' import type { SeoResolveAnswer } from '../app/utils/types.ts' const okAnswer = (over: Partial = {}, staging = false): SeoResolveAnswer => ({ action: 'ok', status: 200, url: 'https://acme.test/about', locale: 'en', staging, seo: { title: 'About | Acme', description: 'Who we are', canonical: 'https://acme.test/about', robots: 'index, follow', x_robots_tag: 'index, follow', og: { title: 'About | Acme', description: 'Who we are', image: 'https://acme.test/a.png', url: 'https://acme.test/about' }, twitter: { card: 'summary_large_image', site: '@acme' }, verification: [], ...over, }, }) test('resolvable URLs drop the hash and skip internal paths', () => { assert.equal(seoResolvableUrl('/blog?page=2#top'), '/blog?page=2') assert.equal(seoResolvableUrl('/'), '/') for (const p of ['/_nuxt/a.js', '/__nuxt_error', '/_journal/api/v1/posts', '/api/x', '/api', '//evil.test/x', 'x']) { assert.equal(seoResolvableUrl(p), null, p) } }) test('seoAnswer accepts the wire shape and rejects anything else', () => { assert.equal(seoAnswer(null), null) assert.equal(seoAnswer({ action: 'weird', status: 200 }), null) assert.equal(seoAnswer({ action: 'ok' }), null) assert.equal(seoAnswer({ action: 'ok', status: 200, seo: 'x' }), null) const a = seoAnswer({ action: 'not_found', status: 404, url: '/x', staging: true }) assert.deepEqual(a && { action: a.action, staging: a.staging, seo: a.seo }, { action: 'not_found', staging: true, seo: null }) }) test('a redirect on the site origin becomes a path, another host stays external', () => { const base = { action: 'redirect' as const, url: 'https://acme.test/old?x=1', locale: '', staging: false, seo: null } assert.deepEqual(seoRedirectTarget({ ...base, status: 301, location: 'https://acme.test/new?x=1' }), { to: '/new?x=1', external: false, code: 301 }) assert.deepEqual(seoRedirectTarget({ ...base, status: 302, location: '/tmp' }), { to: '/tmp', external: false, code: 302 }) assert.deepEqual(seoRedirectTarget({ ...base, status: 301, location: 'https://other.test/p' }), { to: 'https://other.test/p', external: true, code: 301 }) assert.equal(seoRedirectTarget({ ...base, status: 301, location: 'javascript:alert(1)' }), null) assert.equal(seoRedirectTarget({ ...base, status: 301 }), null) assert.equal(seoRedirectTarget(okAnswer()), null) }) test('robots: staging wins, ok answers carry their own, other answers none', () => { assert.equal(seoRobots(okAnswer({}, true)), SEO_STAGING_ROBOTS) assert.equal(seoRobots(okAnswer()), 'index, follow') assert.equal(seoRobots({ action: 'not_found', status: 404, url: '', locale: '', staging: false, seo: null }), '') assert.equal(seoRobots({ action: 'not_found', status: 404, url: '', locale: '', staging: true, seo: null }), SEO_STAGING_ROBOTS) assert.equal(seoRobots(null), '') assert.equal(seoXRobotsTag(okAnswer({}, true)), SEO_STAGING_ROBOTS) assert.equal(seoXRobotsTag(okAnswer()), 'index, follow') }) test('the head of an ok answer has every value and a final title', () => { const head = seoHeadInput(okAnswer({ verification: [{ name: 'google-site-verification', content: 'abc' }] })) assert.equal(head.title, 'About | Acme') assert.equal(head.titleTemplate, '%s') assert.deepEqual(head.link, [{ rel: 'canonical', href: 'https://acme.test/about' }]) const meta = Object.fromEntries(head.meta.map((m) => [m.name ?? m.property, m.content])) assert.deepEqual(meta, { robots: 'index, follow', description: 'Who we are', 'og:title': 'About | Acme', 'og:description': 'Who we are', 'og:image': 'https://acme.test/a.png', 'og:url': 'https://acme.test/about', 'twitter:card': 'summary_large_image', 'twitter:site': '@acme', 'google-site-verification': 'abc', }) }) test('empty values leave the page its own tags', () => { const head = seoHeadInput(okAnswer({ title: '', description: '', canonical: '', og: { title: '', description: '', image: '', url: '' }, twitter: { card: '', site: '' } })) assert.equal(head.title, undefined) assert.equal(head.titleTemplate, undefined) assert.deepEqual(head.link, []) assert.deepEqual(head.meta, [{ name: 'robots', content: 'index, follow' }]) assert.deepEqual(seoHeadInput(null), { meta: [], link: [] }) assert.deepEqual(seoHeadInput({ action: 'not_found', status: 404, url: '', locale: '', staging: true, seo: null }), { meta: [{ name: 'robots', content: SEO_STAGING_ROBOTS }], link: [], }) }) test('verification names: one segment, three extensions, never a reserved name', () => { const reserved = ['robots.txt', 'sitemap.xml', 'rss.xml'] assert.equal(seoVerificationName('/google0123abc.html', reserved), 'google0123abc.html') assert.equal(seoVerificationName('/BingSiteAuth.xml?x=1', reserved), 'BingSiteAuth.xml') for (const p of ['/robots.txt', '/Sitemap.xml', '/rss.xml', '/a/b.html', '/x.php', '/.env.txt', '/', '/x..']) { assert.equal(seoVerificationName(p, reserved), null, p) } }) test('paths in canonical and social URLs become absolute on the request origin', () => { assert.equal(seoAbsolute('/a?b=1', 'https://acme.test'), 'https://acme.test/a?b=1') assert.equal(seoAbsolute('/a', 'https://acme.test/'), 'https://acme.test/a') assert.equal(seoAbsolute('https://x.test/a', 'https://acme.test'), 'https://x.test/a') assert.equal(seoAbsolute('//x.test/a', 'https://acme.test'), '//x.test/a') assert.equal(seoAbsolute('/a', undefined), '/a') const head = seoHeadInput(okAnswer({ canonical: '/about', og: { title: '', description: '', image: '/img.png', url: '/about' } }), 'https://site.test') assert.deepEqual(head.link, [{ rel: 'canonical', href: 'https://site.test/about' }]) const meta = Object.fromEntries(head.meta.map((m) => [m.name ?? m.property, m.content])) assert.equal(meta['og:image'], 'https://site.test/img.png') assert.equal(meta['og:url'], 'https://site.test/about') }) test('seoHeadInput: v0.2.0 keys add html lang, og:locale and hreflang links', () => { const answer = { ...okAnswer({ og: { title: '', description: '', image: '', url: '', locale: 'pl_PL', locale_alternates: ['en_US', 'de_DE'] } }), html_lang: 'pl', alternates: [ { hreflang: 'pl', url: 'https://acme.test/about' }, { hreflang: 'x-default', url: '/about' }, ], } const head = seoHeadInput(answer, 'https://site.test') assert.deepEqual(head.htmlAttrs, { lang: 'pl' }) assert.ok(head.meta.some((m) => m.property === 'og:locale' && m.content === 'pl_PL')) assert.deepEqual( head.meta.filter((m) => m.property === 'og:locale:alternate').map((m) => m.content), ['en_US', 'de_DE'], ) assert.deepEqual( head.link.filter((l) => l.rel === 'alternate'), [ { rel: 'alternate', hreflang: 'pl', href: 'https://acme.test/about', key: 'alt-pl' }, { rel: 'alternate', hreflang: 'x-default', href: 'https://site.test/about', key: 'alt-x-default' }, ], ) }) test('seoHeadInput: an answer without the v0.2.0 keys gives the v0.1.1 head', () => { const head = seoHeadInput(okAnswer(), 'https://site.test') assert.equal(head.htmlAttrs, undefined) assert.ok(!head.meta.some((m) => m.property?.startsWith('og:locale'))) assert.ok(!head.link.some((l) => l.rel === 'alternate')) }) test('seoEnabled: enabled and its alias cms, booleans or strings', () => { assert.equal(seoEnabled(undefined), true) assert.equal(seoEnabled({}), true) assert.equal(seoEnabled({ enabled: true, cms: true }), true) assert.equal(seoEnabled({ enabled: false }), false) assert.equal(seoEnabled({ enabled: 'false' }), false) assert.equal(seoEnabled({ cms: false }), false) assert.equal(seoEnabled({ enabled: true, cms: 'false' }), false) })