/** * SEO Template Engine * * Handles template selection, variable injection, and content generation * for different page types (state, city, category, brand, product, search). */ import { getAllSettings, getSetting } from './settings'; // Page types supported by the template engine export type PageType = 'state' | 'city' | 'category' | 'brand' | 'product' | 'search'; // Template keys mapping export const TEMPLATE_KEYS: Record = { state: 'state_page_template', city: 'city_page_template', category: 'category_page_template', brand: 'brand_page_template', product: 'product_page_template', search: 'search_results_template', }; // Sample mock data for previews export const MOCK_DATA: Record> = { state: { state_name: 'Arizona', state_code: 'AZ', state_code_lower: 'az', dispensary_count: 156, product_count: 12450, brand_count: 287, category_count: 8, top_brands: ['Raw Garden', 'Stiiizy', 'Select', 'Pax', 'Bloom'], top_categories: ['Flower', 'Vape', 'Edibles', 'Concentrate', 'Pre-rolls'], avg_price: 42.50, last_updated: new Date().toISOString().split('T')[0], }, city: { city_name: 'Phoenix', state_name: 'Arizona', state_code: 'AZ', dispensary_count: 45, product_count: 3200, brand_count: 120, nearby_cities: ['Scottsdale', 'Tempe', 'Mesa', 'Glendale'], popular_dispensaries: ['Harvest', 'Curaleaf', 'Zen Leaf'], avg_price: 40.00, }, category: { category_name: 'Flower', category_slug: 'flower', product_count: 4500, brand_count: 95, state_name: 'Arizona', avg_price: 35.00, top_strains: ['Blue Dream', 'OG Kush', 'Girl Scout Cookies'], subcategories: ['Indica', 'Sativa', 'Hybrid'], }, brand: { brand_name: 'Raw Garden', brand_slug: 'raw-garden', product_count: 156, state_presence: ['AZ', 'CA', 'NV', 'CO'], store_count: 89, avg_price: 45.00, categories: ['Concentrate', 'Vape', 'Live Resin'], description: 'Premium cannabis products from California', }, product: { product_name: 'Blue Dream Cartridge', brand_name: 'Select', category: 'Vape', thc_percent: 85.5, cbd_percent: 0.5, price: 45.00, dispensary_name: 'Harvest HOC', dispensary_city: 'Phoenix', state_name: 'Arizona', in_stock: true, }, search: { query: 'live resin', result_count: 245, product_results: 180, dispensary_results: 45, brand_results: 20, state_name: 'Arizona', top_categories: ['Concentrate', 'Vape'], }, }; /** * Apply template variables to a template string * Replaces {{variable}} with values from data object * * Rules: * - Replace {{variable}} occurrences * - Leave unknown variables unchanged * - Prevent undefined values (replace with empty string) * - Support arrays by joining with comma */ export function applyTemplateVariables( template: string, data: Record ): string { if (!template) return ''; let result = template; // Find all {{variable}} patterns const variablePattern = /\{\{(\w+)\}\}/g; let match; while ((match = variablePattern.exec(template)) !== null) { const fullMatch = match[0]; const variableName = match[1]; if (variableName in data) { let value = data[variableName]; // Handle different value types if (value === undefined || value === null) { value = ''; } else if (Array.isArray(value)) { value = value.join(', '); } else if (typeof value === 'object') { value = JSON.stringify(value); } else { value = String(value); } // Replace all occurrences of this variable result = result.split(fullMatch).join(value); } // Leave unknown variables unchanged } return result; } /** * Get the correct template for a page type * Uses case-insensitive matching */ export async function getTemplateForPageType(pageType: string): Promise { const normalizedType = pageType.toLowerCase().trim() as PageType; const templateKey = TEMPLATE_KEYS[normalizedType]; if (!templateKey) { console.warn(`[TemplateEngine] Unknown page type: ${pageType}, falling back to state template`); return getSetting('state_page_template'); } return getSetting(templateKey); } /** * Get regeneration template */ export async function getRegenerationTemplate(): Promise { return getSetting('regeneration_template'); } /** * Generate content for a page using the appropriate template */ export async function generatePageContent( pageType: string, data: Record ): Promise<{ content: string; templateUsed: string; variablesApplied: string[]; }> { const template = await getTemplateForPageType(pageType); const content = applyTemplateVariables(template, data); // Extract which variables were actually used const variablePattern = /\{\{(\w+)\}\}/g; const variablesInTemplate: string[] = []; let match; while ((match = variablePattern.exec(template)) !== null) { if (!variablesInTemplate.includes(match[1])) { variablesInTemplate.push(match[1]); } } const variablesApplied = variablesInTemplate.filter(v => v in data); return { content, templateUsed: TEMPLATE_KEYS[pageType.toLowerCase() as PageType] || 'state_page_template', variablesApplied, }; } /** * Generate a preview with mock data */ export async function generatePreview( pageType: string, customTemplate?: string ): Promise<{ preview: string; template: string; mockData: Record; availableVariables: string[]; }> { const normalizedType = (pageType?.toLowerCase().trim() || 'state') as PageType; const template = customTemplate || await getTemplateForPageType(normalizedType); const mockData = MOCK_DATA[normalizedType] || MOCK_DATA.state; const preview = applyTemplateVariables(template, mockData); return { preview, template, mockData, availableVariables: Object.keys(mockData), }; } /** * Regenerate content using regeneration template */ export async function regenerateContent( pageType: string, originalContent: string, newData: Record, improvementAreas?: string[] ): Promise<{ content: string; regenerationPrompt: string; }> { const regenerationTemplate = await getRegenerationTemplate(); const settings = await getAllSettings(); // Build regeneration context const regenerationData = { ...newData, original_content: originalContent, page_type: pageType, improvement_areas: improvementAreas?.join(', ') || 'SEO keywords, local relevance, data freshness', tone: settings.tone_voice || 'informational', length: settings.default_content_length || 'medium', }; const regenerationPrompt = applyTemplateVariables(regenerationTemplate, regenerationData); // Generate new content using the page template const pageTemplate = await getTemplateForPageType(pageType); const content = applyTemplateVariables(pageTemplate, newData); return { content, regenerationPrompt, }; } /** * Get all available templates and their metadata */ export async function getAllTemplates(): Promise> { const settings = await getAllSettings(); return { state: { key: 'state_page_template', template: settings.state_page_template || '', description: 'Template for state landing pages (e.g., "Arizona Dispensaries")', availableVariables: Object.keys(MOCK_DATA.state), }, city: { key: 'city_page_template', template: settings.city_page_template || '', description: 'Template for city landing pages (e.g., "Phoenix Dispensaries")', availableVariables: Object.keys(MOCK_DATA.city), }, category: { key: 'category_page_template', template: settings.category_page_template || '', description: 'Template for category pages (e.g., "Flower", "Edibles")', availableVariables: Object.keys(MOCK_DATA.category), }, brand: { key: 'brand_page_template', template: settings.brand_page_template || '', description: 'Template for brand pages (e.g., "Raw Garden Products")', availableVariables: Object.keys(MOCK_DATA.brand), }, product: { key: 'product_page_template', template: settings.product_page_template || '', description: 'Template for individual product pages', availableVariables: Object.keys(MOCK_DATA.product), }, search: { key: 'search_results_template', template: settings.search_results_template || '', description: 'Template for search results pages', availableVariables: Object.keys(MOCK_DATA.search), }, regeneration: { key: 'regeneration_template', template: settings.regeneration_template || '', description: 'Template used when regenerating/improving existing content', availableVariables: ['original_content', 'page_type', 'improvement_areas', 'tone', 'length', '...page-specific variables'], }, }; } /** * Validate a template string */ export function validateTemplate(template: string): { valid: boolean; variables: string[]; unknownVariables: string[]; errors: string[]; } { const errors: string[] = []; const variables: string[] = []; // Find all variables const variablePattern = /\{\{(\w+)\}\}/g; let match; while ((match = variablePattern.exec(template)) !== null) { if (!variables.includes(match[1])) { variables.push(match[1]); } } // Check for unclosed brackets const openBrackets = (template.match(/\{\{/g) || []).length; const closeBrackets = (template.match(/\}\}/g) || []).length; if (openBrackets !== closeBrackets) { errors.push('Mismatched template brackets: {{ and }} counts do not match'); } // Check for empty variable names if (template.includes('{{}}')) { errors.push('Empty variable name found: {{}}'); } // Get all known variables const allKnownVariables = new Set(); Object.values(MOCK_DATA).forEach(data => { Object.keys(data).forEach(key => allKnownVariables.add(key)); }); allKnownVariables.add('original_content'); allKnownVariables.add('page_type'); allKnownVariables.add('improvement_areas'); allKnownVariables.add('tone'); allKnownVariables.add('length'); const unknownVariables = variables.filter(v => !allKnownVariables.has(v)); return { valid: errors.length === 0, variables, unknownVariables, errors, }; }