- extend vue-seo-layer (link dependency during development) and drop
@nuxtjs/seo: the page head, canonical and the one JSON-LD graph come
from the CMS resolve answer; the post's own title, excerpt and image
apply only while there is no CMS answer
- remove the journal sitemap source and the copied shared purge route,
library and plugin (the SEO layer serves POST /api/__site__/cache/purge);
the deprecated POST /api/__journal__/cache/purge keeps working
- post title is h1 while the Journal API reports content_h1_to_h2 on
(or absent) and p.journal-post__title while off; journal.postTitleTag
('auto' | 'h1' | 'p') overrides it; body h1 headings are demoted to h2
while the setting is on (gateJournalHtml/sanitizeJournalHtml demoteH1)
- journal.breadcrumbs option and post-title CSS tokens
- smoke harness: count=, header=, noheader= and resolves lines; the
fixture API also answers the SEO resolve and site endpoints
BREAKING CHANGE: the layer no longer installs @nuxtjs/seo (no
nuxt-site-config, nuxt-schema-org, robots or sitemap module, no
/api/__sitemap__/journal source, no useSiteConfig()); apps take their
head, sitemaps and robots from the SEO plugin through vue-seo-layer.
122 lines
5.3 KiB
TypeScript
122 lines
5.3 KiB
TypeScript
// SEO values for Journal pages: description fallback, absolute og:image and
|
|
// list titles.
|
|
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { absoluteUrl, cmsHeadPresent, htmlToText, journalPostTitleTag, journalSiteOrigin, listTitle, postSeoMeta, truncateText } from '../app/utils/seo.ts'
|
|
import type { JournalPost } from '../app/utils/types.ts'
|
|
|
|
function post(extra: Partial<JournalPost> = {}): JournalPost {
|
|
return {
|
|
id: 1,
|
|
title: 'Najnowsze Badania nad Cordycepsem',
|
|
slug: 'blogbadania-nad-cordycepsem',
|
|
excerpt: null,
|
|
content: null,
|
|
content_html: null,
|
|
reading_time: 1,
|
|
published: true,
|
|
published_at: '2024-06-12T06:00:00+00:00',
|
|
is_pinned: false,
|
|
metadata: null,
|
|
sources: null,
|
|
author: null,
|
|
categories: [],
|
|
tags: [],
|
|
featured_images: [],
|
|
featured_image_url: null,
|
|
translations: null,
|
|
created_at: '2024-06-12T06:00:00+00:00',
|
|
updated_at: '2024-06-13T06:00:00+00:00',
|
|
...extra,
|
|
}
|
|
}
|
|
|
|
test('htmlToText drops tags, scripts and styles and decodes entities', () => {
|
|
assert.equal(
|
|
htmlToText('<p>A &<b> "c" 'd'</p><script>x()</script><style>p{}</style>\n<p>e</p>'),
|
|
'A &<b> "c" \'d\' e',
|
|
)
|
|
assert.equal(htmlToText(null), '')
|
|
})
|
|
|
|
test('truncateText keeps short text and cuts long text at a word with an ellipsis', () => {
|
|
assert.equal(truncateText('krótki', 160), 'krótki')
|
|
const long = 'słowo '.repeat(40).trim()
|
|
const cut = truncateText(long, 160)
|
|
assert.ok(cut.length <= 160)
|
|
assert.ok(cut.endsWith('…'))
|
|
assert.ok(!cut.endsWith(' …'))
|
|
assert.equal(truncateText('x'.repeat(200), 10), `${'x'.repeat(9)}…`)
|
|
})
|
|
|
|
test('the excerpt is the description when set', () => {
|
|
const meta = postSeoMeta(post({ excerpt: 'Co mówi nauka.', content_html: '<p>Treść</p>' }))
|
|
assert.equal(meta.description, 'Co mówi nauka.')
|
|
assert.equal(meta.ogDescription, 'Co mówi nauka.')
|
|
assert.equal(meta.title, 'Najnowsze Badania nad Cordycepsem')
|
|
assert.equal(meta.ogTitle, meta.title)
|
|
assert.equal(meta.ogType, 'article')
|
|
})
|
|
|
|
test('without an excerpt the description is the first 160 characters of the body text', () => {
|
|
const body = `<p>${'Grzyby funkcjonalne wspierają zdrowie. '.repeat(10)}</p>`
|
|
const meta = postSeoMeta(post({ excerpt: '', content_html: body }))
|
|
assert.ok(meta.description.length <= 160)
|
|
assert.ok(meta.description.startsWith('Grzyby funkcjonalne wspierają zdrowie.'))
|
|
assert.ok(!meta.description.includes('<'))
|
|
})
|
|
|
|
test('og:image becomes absolute against the site URL', () => {
|
|
const url = '/storage/app/uploads/public/665/f1e/2a3/665f1e2a3b4c5d6e7f8091.png'
|
|
assert.equal(postSeoMeta(post({ featured_image_url: url }), 'https://grzybyfunkcjonalne.pl/').ogImage, `https://grzybyfunkcjonalne.pl${url}`)
|
|
assert.equal(postSeoMeta(post({ featured_image_url: 'https://cdn.test/a.png' }), 'https://x.test').ogImage, 'https://cdn.test/a.png')
|
|
assert.equal(postSeoMeta(post()).ogImage, undefined)
|
|
})
|
|
|
|
test('article times come from published_at and updated_at', () => {
|
|
const meta = postSeoMeta(post())
|
|
assert.equal(meta.articlePublishedTime, '2024-06-12T06:00:00+00:00')
|
|
assert.equal(meta.articleModifiedTime, '2024-06-13T06:00:00+00:00')
|
|
const bare = postSeoMeta(post({ published_at: null, updated_at: null }))
|
|
assert.equal(bare.articlePublishedTime, undefined)
|
|
assert.equal(bare.articleModifiedTime, undefined)
|
|
})
|
|
|
|
test('absoluteUrl passes through absolute URLs and a missing site URL', () => {
|
|
assert.equal(absoluteUrl('a.png', 'https://x.test'), 'https://x.test/a.png')
|
|
assert.equal(absoluteUrl('HTTPS://y.test/a.png', 'https://x.test'), 'HTTPS://y.test/a.png')
|
|
assert.equal(absoluteUrl('/a.png', null), '/a.png')
|
|
})
|
|
|
|
test('listTitle adds the page title only after page 1', () => {
|
|
assert.equal(listTitle('Artykuły', 1, 'Strona 1'), 'Artykuły')
|
|
assert.equal(listTitle('Artykuły', 2, 'Strona 2'), 'Artykuły · Strona 2')
|
|
})
|
|
|
|
test('journalPostTitleTag follows the Journal H1 setting unless the option forces a tag', () => {
|
|
assert.equal(journalPostTitleTag('auto', true), 'h1')
|
|
assert.equal(journalPostTitleTag('auto', undefined), 'h1')
|
|
assert.equal(journalPostTitleTag('auto', null), 'h1')
|
|
assert.equal(journalPostTitleTag('auto', false), 'p')
|
|
assert.equal(journalPostTitleTag(undefined, false), 'p')
|
|
assert.equal(journalPostTitleTag('h2', false), 'p')
|
|
assert.equal(journalPostTitleTag('h1', false), 'h1')
|
|
assert.equal(journalPostTitleTag('p', true), 'p')
|
|
})
|
|
|
|
test('cmsHeadPresent is true only for an ok answer with head values', () => {
|
|
assert.equal(cmsHeadPresent({ action: 'ok', seo: { title: 'x' } }), true)
|
|
assert.equal(cmsHeadPresent({ action: 'ok', seo: null }), false)
|
|
assert.equal(cmsHeadPresent({ action: 'not_found', seo: null }), false)
|
|
assert.equal(cmsHeadPresent(null), false)
|
|
assert.equal(cmsHeadPresent(undefined), false)
|
|
})
|
|
|
|
test('journalSiteOrigin prefers the CMS page URL origin over the request origin', () => {
|
|
assert.equal(journalSiteOrigin('https://acme.test/blog/x', 'http://127.0.0.1:3000'), 'https://acme.test')
|
|
assert.equal(journalSiteOrigin('/blog/x', 'http://127.0.0.1:3000'), 'http://127.0.0.1:3000')
|
|
assert.equal(journalSiteOrigin('', 'http://127.0.0.1:3000'), 'http://127.0.0.1:3000')
|
|
assert.equal(journalSiteOrigin(null, 'http://127.0.0.1:3000'), 'http://127.0.0.1:3000')
|
|
assert.equal(journalSiteOrigin('javascript:alert(1)', 'http://h.test'), 'http://h.test')
|
|
})
|