feat: SEO SPA editor integration
This commit is contained in:
1 parent
eb9876fde3
commit
470584400f
62 files changed
+7471
-58
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isChecking } from '@/composables/useSeoCheck'
|
||||
import { suggestionLinkLabel, suggestionMessage } from '@/lib/seoCopy'
|
||||
import type { SeoSuggestion, SuggestionCode } from '@/lib/seoTypes'
|
||||
|
||||
function suggestion(code: string, params: Record<string, number> = {}): SeoSuggestion {
|
||||
return { code: code as SuggestionCode, field: 'body', params }
|
||||
}
|
||||
|
||||
describe('suggestionMessage', () => {
|
||||
it('uses the same wording for short and long descriptions with the length filled in', () => {
|
||||
const short = suggestionMessage(suggestion('desc_short', { length: 12 }))
|
||||
const long = suggestionMessage(suggestion('desc_long', { length: 200 }))
|
||||
|
||||
expect(short).toContain('12')
|
||||
expect(long).toContain('200')
|
||||
expect(short.replace('12', '#')).toBe(long.replace('200', '#'))
|
||||
})
|
||||
|
||||
it('switches between the singular and plural body image wording', () => {
|
||||
const one = suggestionMessage(suggestion('body_alt', { count: 1 }))
|
||||
const many = suggestionMessage(suggestion('body_alt', { count: 3 }))
|
||||
|
||||
expect(one).not.toBe(many)
|
||||
expect(many).toContain('3')
|
||||
expect(one).not.toContain('1 images')
|
||||
})
|
||||
|
||||
it('falls back to the raw code for an unknown code', () => {
|
||||
expect(suggestionMessage(suggestion('brand_new'))).toBe('brand_new')
|
||||
})
|
||||
})
|
||||
|
||||
describe('suggestionLinkLabel', () => {
|
||||
it('returns an empty label for an unknown field', () => {
|
||||
expect(suggestionLinkLabel('nope' as never)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isChecking', () => {
|
||||
it.each([
|
||||
['idle', true],
|
||||
['pending', true],
|
||||
['ready', false],
|
||||
['failed', false],
|
||||
] as const)('%s is %s', (phase, expected) => {
|
||||
expect(isChecking(phase)).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { EMPTY_SEO } from '@/lib/articleSeo'
|
||||
import {
|
||||
applyTitleTemplate,
|
||||
bodyText,
|
||||
firstSentence,
|
||||
previewTitle,
|
||||
siteDisplayName,
|
||||
socialTitle,
|
||||
} from '@/lib/seoPreview'
|
||||
import type { Site } from '@/composables/useSites'
|
||||
|
||||
const site = {
|
||||
id: 1,
|
||||
name: 'Renew',
|
||||
key: 'renew',
|
||||
base_url: 'https://renew.example',
|
||||
default_social_image_id: null,
|
||||
default_robots: 'index',
|
||||
locale: null,
|
||||
site_name: '',
|
||||
title_template: '%title% | %site%',
|
||||
is_default: true,
|
||||
article_count: 1,
|
||||
} as Site
|
||||
|
||||
describe('applyTitleTemplate', () => {
|
||||
it('replaces every occurrence in a single pass', () => {
|
||||
expect(applyTitleTemplate('%site%: %title% (%title%)', 'A', 'S')).toBe('S: A (A)')
|
||||
})
|
||||
|
||||
it('does not expand replacement patterns or substitute tokens inside the title', () => {
|
||||
expect(applyTitleTemplate('%title% | %site%', "Cost $& $' $` %site%", 'Renew')).toBe(
|
||||
"Cost $& $' $` %site% | Renew",
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to the default template and trims', () => {
|
||||
expect(applyTitleTemplate('', 'A', '')).toBe('A |')
|
||||
})
|
||||
})
|
||||
|
||||
describe('bodyText', () => {
|
||||
it('separates block elements with a space', () => {
|
||||
expect(bodyText('<p>One.</p><p>Two</p>')).toBe('One. Two')
|
||||
expect(firstSentence('<p>One.</p><p>Two</p>')).toBe('One.')
|
||||
})
|
||||
|
||||
it('skips script and style text', () => {
|
||||
expect(bodyText('<style>p{}</style><p>Kept</p><script>alert(1)</script>')).toBe('Kept')
|
||||
})
|
||||
|
||||
it('does not run markup', () => {
|
||||
const img = '<img src=x onerror="document.body.dataset.ran=1">'
|
||||
bodyText(img)
|
||||
expect(document.body.dataset.ran).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('titles', () => {
|
||||
const input = { seo: { ...EMPTY_SEO }, title: 'Article', body: '', site }
|
||||
|
||||
it('uses the site name, then the site key name, for the template', () => {
|
||||
expect(siteDisplayName(site)).toBe('Renew')
|
||||
expect(previewTitle(input)).toBe('Article | Renew')
|
||||
})
|
||||
|
||||
it('falls back to the templated title for the social title', () => {
|
||||
expect(socialTitle(input)).toBe('Article | Renew')
|
||||
expect(socialTitle({ ...input, seo: { ...EMPTY_SEO, seo_title: 'Meta' } })).toBe(
|
||||
'Meta | Renew',
|
||||
)
|
||||
expect(socialTitle({ ...input, seo: { ...EMPTY_SEO, social_title: 'Own' } })).toBe('Own')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user