UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

407 lines (406 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.fetchContentForRoute = fetchContentForRoute; exports.fetchContentBySlug = fetchContentBySlug; exports.fetchContentById = fetchContentById; exports.validateContentRoute = validateContentRoute; exports.preloadRouteContent = preloadRouteContent; exports.getAvailableRoutes = getAvailableRoutes; exports.contentExistsForRoute = contentExistsForRoute; const content_fetching_1 = require("./content-fetching"); const route_config_1 = require("./route-config"); const error_handling_1 = require("./error-handling"); const retry_1 = require("./retry"); // ============================================================================ // CONTENT PROJECTION TEMPLATES // ============================================================================ /** * Default projection for router content */ const DEFAULT_ROUTER_PROJECTION = ` _id, _type, _createdAt, _updatedAt, title, slug, content, publishedAt, status ` .replace(/\s+/g, ' ') .trim(); /** * SEO-enhanced projection */ const SEO_ENHANCED_PROJECTION = ` ${DEFAULT_ROUTER_PROJECTION}, seo->{ title, description, keywords, image, noIndex, noFollow } ` .replace(/\s+/g, ' ') .trim(); /** * Author-enhanced projection */ const AUTHOR_ENHANCED_PROJECTION = ` ${DEFAULT_ROUTER_PROJECTION}, author->{ _id, name, slug, image, bio } ` .replace(/\s+/g, ' ') .trim(); /** * Full projection with SEO and author */ const FULL_PROJECTION = ` ${DEFAULT_ROUTER_PROJECTION}, seo->{ title, description, keywords, image, noIndex, noFollow }, author->{ _id, name, slug, image, bio }, categories[]->{ _id, title, slug }, tags[]->{ _id, title, slug } ` .replace(/\s+/g, ' ') .trim(); // ============================================================================ // CORE CONTENT FETCHING FUNCTIONS // ============================================================================ /** * Fetch content for a specific route path */ async function fetchContentForRoute(path, options = {}) { const routeConfig = options.routeConfig ? { ...(0, route_config_1.getRouteConfig)(), ...options.routeConfig } : (0, route_config_1.getRouteConfig)(); // Validate if this is a CMS route if (!(0, route_config_1.isValidCmsRoute)(path, routeConfig)) { return null; } // Extract route information const routeInfo = (0, route_config_1.extractRouteInfo)(path, routeConfig); if (!routeInfo.matched || !routeInfo.slug || !routeInfo.contentType) { return null; } return (0, retry_1.withRetry)(async () => { try { // Build projection based on options const projection = buildProjection(options); // Fetch content using existing function const document = await (0, content_fetching_1.getDocumentBySlug)(routeInfo.contentType, routeInfo.slug, { projection, ...(options.includeDrafts !== undefined && { includeDrafts: options.includeDrafts }), }); if (!document) { return null; } // Transform to CmsContent format return transformToCmsContent(document); } catch (error) { throw (0, error_handling_1.createSanityError)(error, error_handling_1.SANITY_ERROR_CODES.FETCH_FAILED, { operation: 'fetchContentForRoute', path, contentType: routeInfo.contentType, slug: routeInfo.slug, }); } }, retry_1.RETRY_PRESETS.standard); } /** * Fetch content by content type and slug */ async function fetchContentBySlug(contentType, slug, options = {}) { return (0, retry_1.withRetry)(async () => { try { const projection = buildProjection(options); const document = await (0, content_fetching_1.getDocumentBySlug)(contentType, slug, { projection, ...(options.includeDrafts !== undefined && { includeDrafts: options.includeDrafts }), }); if (!document) { return null; } return transformToCmsContent(document); } catch (error) { throw (0, error_handling_1.createSanityError)(error, error_handling_1.SANITY_ERROR_CODES.FETCH_FAILED, { operation: 'fetchContentBySlug', contentType, slug, }); } }, retry_1.RETRY_PRESETS.standard); } /** * Fetch content by document ID */ async function fetchContentById(contentId, options = {}) { return (0, retry_1.withRetry)(async () => { try { const projection = buildProjection(options); const document = await (0, content_fetching_1.getDocumentById)(contentId, { projection, ...(options.includeDrafts !== undefined && { includeDrafts: options.includeDrafts }), }); if (!document) { return null; } return transformToCmsContent(document); } catch (error) { throw (0, error_handling_1.createSanityError)(error, error_handling_1.SANITY_ERROR_CODES.FETCH_FAILED, { operation: 'fetchContentById', contentId, }); } }, retry_1.RETRY_PRESETS.standard); } // ============================================================================ // ROUTE VALIDATION WITH CONTENT // ============================================================================ /** * Validate a route and fetch its content if available */ async function validateContentRoute(path, options = {}) { const routeConfig = options.routeConfig ? { ...(0, route_config_1.getRouteConfig)(), ...options.routeConfig } : (0, route_config_1.getRouteConfig)(); try { // Check if path is a valid CMS route if (!(0, route_config_1.isValidCmsRoute)(path, routeConfig)) { return { isValid: false, error: 'Path is not a valid CMS route', }; } // Extract route information const routeInfo = (0, route_config_1.extractRouteInfo)(path, routeConfig); if (!routeInfo.matched) { return { isValid: false, error: 'Path does not match any configured patterns', routeInfo, }; } if (!routeInfo.slug || !routeInfo.contentType) { return { isValid: false, error: 'Could not extract content type and slug from path', routeInfo, }; } // Try to fetch content const content = await fetchContentForRoute(path, options); const result = { isValid: content !== null, contentType: routeInfo.contentType, slug: routeInfo.slug, routeInfo, }; if (content) { result.content = content; } else { result.error = 'Content not found'; } return result; } catch (error) { return { isValid: false, error: error instanceof error_handling_1.SanityError ? error.message : String(error), }; } } // ============================================================================ // BATCH OPERATIONS // ============================================================================ /** * Preload content for multiple routes */ async function preloadRouteContent(paths, options = {}) { const startTime = Date.now(); const maxRoutes = options.maxRoutes || paths.length; const pathsToProcess = paths.slice(0, maxRoutes); const loaded = new Map(); const failed = new Map(); if (options.parallel !== false) { // Parallel processing const results = await Promise.allSettled(pathsToProcess.map(async (path) => { const content = await fetchContentForRoute(path, options); return { path, content }; })); results.forEach((result, index) => { const path = pathsToProcess[index]; if (result.status === 'fulfilled' && result.value.content) { loaded.set(path, result.value.content); } else { const error = result.status === 'rejected' ? result.reason : 'Content not found'; failed.set(path, String(error)); } }); } else { // Sequential processing for (const path of pathsToProcess) { try { const content = await fetchContentForRoute(path, options); if (content) { loaded.set(path, content); } else { failed.set(path, 'Content not found'); } } catch (error) { failed.set(path, String(error)); } } } return { loaded, failed, duration: Date.now() - startTime, totalRoutes: pathsToProcess.length, }; } /** * Get available routes for a content type */ async function getAvailableRoutes(contentType, options = {}) { const routeConfig = (0, route_config_1.getRouteConfig)(); const contentTypes = contentType ? [contentType] : ['post', 'page']; const routes = []; for (const type of contentTypes) { try { const documents = await (0, content_fetching_1.getDocumentsByType)(type, { projection: '_id, title, slug, publishedAt, status, _updatedAt', ...(options.includeDrafts !== undefined && { includeDrafts: options.includeDrafts }), limit: 100, // Reasonable limit for route generation }); for (const doc of documents.documents) { const docAny = doc; if (docAny.slug?.current) { // Build path based on route patterns const path = buildPathForContentType(type, docAny.slug.current, routeConfig); routes.push({ path, contentType: type, slug: docAny.slug.current, title: docAny.title || 'Untitled', contentId: docAny._id, isPublished: docAny.status === 'published', lastModified: docAny._updatedAt, }); } } } catch (error) { // Continue with other content types if one fails console.warn(`Failed to fetch routes for content type ${type}:`, error); } } return routes; } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ /** * Build projection string based on options */ function buildProjection(options) { if (options.projection) { return options.projection; } if (options.includeSEO && options.includeAuthor) { return FULL_PROJECTION; } if (options.includeSEO) { return SEO_ENHANCED_PROJECTION; } if (options.includeAuthor) { return AUTHOR_ENHANCED_PROJECTION; } return DEFAULT_ROUTER_PROJECTION; } /** * Transform Sanity document to CmsContent format */ function transformToCmsContent(document) { return { _id: document._id, _type: document._type, slug: document.slug?.current || '', title: document.title || 'Untitled', content: document.content || document.body || null, metadata: document.seo, publishedAt: document.publishedAt, author: document.author, }; } /** * Build path for content type and slug based on route patterns */ function buildPathForContentType(contentType, slug, routeConfig) { // Find the first pattern that matches this content type const pattern = routeConfig.patterns.find(p => p.enabled !== false && p.contentType === contentType); if (pattern) { // Extract path template from pattern if (pattern.pattern.includes('/blog/')) { return `/blog/${slug}`; } if (pattern.pattern.includes('/docs/')) { return `/docs/${slug}`; } } // Default to slug-based path return `/${slug}`; } /** * Check if content exists for a given route */ async function contentExistsForRoute(path, options = {}) { try { const content = await fetchContentForRoute(path, { ...options, projection: '_id', // Minimal projection for existence check }); return content !== null; } catch { return false; } }