125 lines
6.1 KiB
TypeScript
125 lines
6.1 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import {
|
|
SEO_STAGING_ROBOTS,
|
|
seoAbsolute,
|
|
seoAnswer,
|
|
seoHeadInput,
|
|
seoRedirectTarget,
|
|
seoResolvableUrl,
|
|
seoRobots,
|
|
seoVerificationName,
|
|
seoXRobotsTag,
|
|
} from '../app/utils/seo.ts'
|
|
import type { SeoResolveAnswer } from '../app/utils/types.ts'
|
|
|
|
const okAnswer = (over: Partial<SeoResolveAnswer['seo']> = {}, 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')
|
|
})
|