@bernierllc/content-type-blog-post
Version:
Blog post content type with rich TipTap editor, SEO metadata, database storage, and web publishing
172 lines (157 loc) • 5.54 kB
text/typescript
/*
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license.
The client may use and modify this code *only within the scope of the project it was delivered for*.
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
*/
import { validateSEOCompleteness, checkSEOLimits } from '../src/seo';
import { createSampleBlogPost } from './test-utils';
describe('seo', () => {
describe('validateSEOCompleteness', () => {
it('should pass validation for complete SEO', () => {
const post = createSampleBlogPost();
const result = validateSEOCompleteness(post);
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
});
it('should fail if meta title is missing', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: '',
metaDescription: 'Description',
keywords: ['test'],
ogType: 'article',
},
});
const result = validateSEOCompleteness(post);
expect(result.success).toBe(false);
expect(result.error).toContain('Meta title is required');
});
it('should fail if meta description is missing', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: 'Title',
metaDescription: '',
keywords: ['test'],
ogType: 'article',
},
});
const result = validateSEOCompleteness(post);
expect(result.success).toBe(false);
expect(result.error).toContain('Meta description is required');
});
it('should fail if keywords are missing', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: 'Title',
metaDescription: 'Description',
keywords: [],
ogType: 'article',
},
});
const result = validateSEOCompleteness(post);
expect(result.success).toBe(false);
expect(result.error).toContain('At least one keyword is required');
});
it('should report multiple validation errors', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: '',
metaDescription: '',
keywords: [],
ogType: 'article',
},
});
const result = validateSEOCompleteness(post);
expect(result.success).toBe(false);
expect(result.error).toContain('Meta title is required');
expect(result.error).toContain('Meta description is required');
expect(result.error).toContain('At least one keyword is required');
});
it('should accept whitespace-only fields as invalid', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: ' ',
metaDescription: ' ',
keywords: ['test'],
ogType: 'article',
},
});
const result = validateSEOCompleteness(post);
expect(result.success).toBe(false);
});
});
describe('checkSEOLimits', () => {
it('should return no warnings for SEO within limits', () => {
const post = createSampleBlogPost();
const warnings = checkSEOLimits(post);
expect(warnings).toEqual([]);
});
it('should warn if meta title exceeds 60 characters', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: 'A'.repeat(70),
metaDescription: 'Description',
keywords: ['test'],
ogType: 'article',
},
});
const warnings = checkSEOLimits(post);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('Meta title is 70 characters');
expect(warnings[0]).toContain('recommended: 60 or less');
});
it('should warn if meta description exceeds 160 characters', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: 'Title',
metaDescription: 'A'.repeat(200),
keywords: ['test'],
ogType: 'article',
},
});
const warnings = checkSEOLimits(post);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('Meta description is 200 characters');
expect(warnings[0]).toContain('recommended: 160 or less');
});
it('should warn if keywords exceed 10', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: 'Title',
metaDescription: 'Description',
keywords: Array(15).fill('keyword'),
ogType: 'article',
},
});
const warnings = checkSEOLimits(post);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('15 keywords provided');
expect(warnings[0]).toContain('recommended: 10 or less');
});
it('should return multiple warnings when multiple limits exceeded', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: 'A'.repeat(70),
metaDescription: 'B'.repeat(200),
keywords: Array(15).fill('keyword'),
ogType: 'article',
},
});
const warnings = checkSEOLimits(post);
expect(warnings).toHaveLength(3);
});
it('should not warn at exact limits', () => {
const post = createSampleBlogPost({
seo: {
metaTitle: 'A'.repeat(60),
metaDescription: 'B'.repeat(160),
keywords: Array(10).fill('keyword'),
ogType: 'article',
},
});
const warnings = checkSEOLimits(post);
expect(warnings).toEqual([]);
});
});
});