UNPKG

@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
/** * @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), }; }