@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
281 lines (280 loc) • 8.92 kB
JavaScript
/**
* @fileoverview Content Analysis Utilities for SEO
*
* This module provides functions for analyzing content to determine
* appropriate SEO characteristics, schema types, and indexing rules.
*/
/**
* Analyze content to determine SEO characteristics
*
* @param content - Content data to analyze
* @param template - Template hint for content type
* @returns Content analysis result
*/
export function analyzeContent(content, template) {
// Default result for unknown content
const defaultResult = {
type: 'unknown',
isPublished: false,
shouldIndex: false,
schemaType: 'WebPage',
ogType: 'website',
};
// Handle template-based detection
if (template) {
switch (template) {
case 'home':
return {
type: 'home',
isPublished: true,
shouldIndex: true,
schemaType: 'WebSite',
ogType: 'website',
};
case 'article':
return {
type: 'article',
isPublished: content?.status === 'published' || !content?.status,
shouldIndex: content?.status === 'published' || !content?.status,
schemaType: 'Article',
ogType: 'article',
};
case 'page':
return {
type: 'page',
isPublished: content?.status === 'published' || !content?.status,
shouldIndex: content?.status === 'published' || !content?.status,
schemaType: 'WebPage',
ogType: 'website',
};
case 'author':
return {
type: 'author',
isPublished: true,
shouldIndex: true,
schemaType: 'Person',
ogType: 'profile',
};
case 'category':
return {
type: 'category',
isPublished: true,
shouldIndex: true,
schemaType: 'CollectionPage',
ogType: 'website',
};
}
}
// Handle content-based detection
if (!content) {
return defaultResult;
}
// Determine content type from Sanity document type
switch (content._type) {
case 'post':
return analyzePost(content);
case 'page':
return analyzePage(content);
case 'author':
return analyzeAuthor(content);
case 'category':
return analyzeCategory(content);
default:
return analyzeGenericContent(content);
}
}
/**
* Analyze a blog post for SEO characteristics
*
* @param post - Post data
* @returns Content analysis result
*/
export function analyzePost(post) {
const isPublished = post.status === 'published' || (!post.status && post.publishedAt !== undefined);
return {
type: 'article',
isPublished,
shouldIndex: isPublished && !post.seo?.noIndex,
schemaType: determineArticleSchemaType(post),
ogType: 'article',
};
}
/**
* Analyze a page for SEO characteristics
*
* @param page - Page data
* @returns Content analysis result
*/
export function analyzePage(page) {
const isPublished = page.status === 'published' || !page.status;
return {
type: 'page',
isPublished,
shouldIndex: isPublished && !page.seo?.noIndex,
schemaType: 'WebPage',
ogType: 'website',
};
}
/**
* Analyze an author for SEO characteristics
*
* @param author - Author data
* @returns Content analysis result
*/
export function analyzeAuthor(author) {
return {
type: 'author',
isPublished: true,
shouldIndex: true,
schemaType: 'Person',
ogType: 'profile',
};
}
/**
* Analyze a category for SEO characteristics
*
* @param category - Category data
* @returns Content analysis result
*/
export function analyzeCategory(category) {
return {
type: 'category',
isPublished: true,
shouldIndex: true,
schemaType: 'CollectionPage',
ogType: 'website',
};
}
/**
* Analyze generic content for SEO characteristics
*
* @param content - Generic content data
* @returns Content analysis result
*/
export function analyzeGenericContent(content) {
const isPublished = content.status === 'published' || !content.status || content.publishedAt !== undefined;
return {
type: 'page',
isPublished,
shouldIndex: isPublished && !content.seo?.noIndex,
schemaType: 'WebPage',
ogType: 'website',
};
}
/**
* Determine the appropriate Schema.org article type
*
* @param post - Post data
* @returns Schema.org article type
*/
export function determineArticleSchemaType(post) {
// Check categories or tags for news indicators
const categories = post.categories || [];
const hasNewsCategory = categories.some((cat) => cat.title?.toLowerCase().includes('news') || cat.slug?.current?.includes('news'));
if (hasNewsCategory) {
return 'NewsArticle';
}
// Check for blog indicators
const hasBlogCategory = categories.some((cat) => cat.title?.toLowerCase().includes('blog') || cat.slug?.current?.includes('blog'));
if (hasBlogCategory) {
return 'BlogPosting';
}
// Default to Article
return 'Article';
}
/**
* Check if content should be indexed by search engines
*
* @param content - Content to check
* @param contentAnalysis - Content analysis result
* @returns Whether content should be indexed
*/
export function shouldIndexContent(content, contentAnalysis) {
// Don't index if explicitly set to noIndex
if (content?.seo?.noIndex) {
return false;
}
// Don't index draft content
if (content?.status === 'draft') {
return false;
}
// Don't index archived content
if (content?.status === 'archived') {
return false;
}
// Index if published or no status (default published)
return contentAnalysis.isPublished;
}
/**
* Determine content freshness for SEO
*
* @param content - Content to analyze
* @returns Content freshness information
*/
export function analyzeContentFreshness(content) {
const now = new Date();
const publishedAt = content?.publishedAt ? new Date(content.publishedAt) : null;
const modifiedAt = content?._updatedAt ? new Date(content._updatedAt) : null;
const daysSincePublished = publishedAt
? Math.floor((now.getTime() - publishedAt.getTime()) / (1000 * 60 * 60 * 24))
: Number.MAX_SAFE_INTEGER;
const daysSinceModified = modifiedAt
? Math.floor((now.getTime() - modifiedAt.getTime()) / (1000 * 60 * 60 * 24))
: Number.MAX_SAFE_INTEGER;
return {
isRecent: daysSincePublished <= 30, // Published within last 30 days
daysSincePublished,
daysSinceModified,
needsUpdate: daysSinceModified > 90, // Not updated in 90+ days
};
}
/**
* Extract content quality indicators for SEO
*
* @param content - Content to analyze
* @returns Quality indicators
*/
export function analyzeContentQuality(content) {
const hasImage = !!(content?.mainImage || content?.image);
const hasDescription = !!(content?.excerpt || content?.description || content?.seo?.description);
const hasCategories = !!(content?.categories && content.categories.length > 0);
const hasAuthor = !!content?.author;
// Estimate word count from content
let wordCount = 0;
if (content?.body && Array.isArray(content.body)) {
const text = content.body
.filter((block) => block._type === 'block')
.map((block) => block.children
?.filter((child) => child._type === 'span')
?.map((span) => span.text)
?.join(''))
.join(' ');
wordCount = text.split(/\s+/).filter(word => word.length > 0).length;
}
const readingTime = Math.ceil(wordCount / 200); // 200 words per minute
// Calculate quality score (0-100)
let qualityScore = 0;
if (hasImage)
qualityScore += 20;
if (hasDescription)
qualityScore += 20;
if (hasCategories)
qualityScore += 10;
if (hasAuthor)
qualityScore += 10;
if (wordCount >= 300)
qualityScore += 20; // Substantial content
if (wordCount >= 1000)
qualityScore += 10; // In-depth content
if (content?.title && content.title.length >= 30)
qualityScore += 10; // Descriptive title
return {
hasImage,
hasDescription,
hasCategories,
hasAuthor,
wordCount,
readingTime,
qualityScore: Math.min(qualityScore, 100),
};
}