From 48eebc9c156736758d98a4b014a89999e206067b Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Sun, 8 Jun 2025 20:54:10 -0500 Subject: [PATCH 1/5] fix(homepage): update template file extensions from .liquid to .json for blog, article, and search templates --- lib/store-renderer/renderers/homepage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/store-renderer/renderers/homepage.ts b/lib/store-renderer/renderers/homepage.ts index d6a98557..54190270 100644 --- a/lib/store-renderer/renderers/homepage.ts +++ b/lib/store-renderer/renderers/homepage.ts @@ -291,9 +291,9 @@ export class DynamicPageRenderer { product: 'templates/product.json', collection: 'templates/collection.json', page: 'templates/page.json', - blog: 'templates/blog.liquid', - article: 'templates/article.liquid', - search: 'templates/search.liquid', + blog: 'templates/blog.json', + article: 'templates/article.json', + search: 'templates/search.json', cart: 'templates/cart.json', '404': 'templates/404.json', } From db763433a7c4f781ea4ddb78f3fb146098a0811e Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Sun, 8 Jun 2025 21:43:05 -0500 Subject: [PATCH 2/5] feat(schema): enhance schema validation and error handling; add secondary index for title in resource schema, improve collection transformation logic, and register new form tag in Liquid engine --- amplify/data/resource.ts | 2 +- lib/store-renderer/liquid/engine.ts | 2 + lib/store-renderer/liquid/tags/form-tag.ts | 230 ++++++++++++++++++ .../services/fetchers/collection-fetcher.ts | 4 +- .../services/rendering/section-renderer.ts | 33 ++- .../services/templates/schema-parser.ts | 152 +++++++++++- 6 files changed, 412 insertions(+), 11 deletions(-) create mode 100644 lib/store-renderer/liquid/tags/form-tag.ts diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index f0037990..e37ef6c0 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -179,7 +179,7 @@ const schema = a owner: a.string().required(), products: a.hasMany('Product', 'collectionId'), }) - .secondaryIndexes(index => [index('storeId')]) + .secondaryIndexes(index => [index('storeId'), index('title')]) .authorization(allow => [ allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']), allow.guest().to(['read']), diff --git a/lib/store-renderer/liquid/engine.ts b/lib/store-renderer/liquid/engine.ts index 96b499f6..282a9806 100644 --- a/lib/store-renderer/liquid/engine.ts +++ b/lib/store-renderer/liquid/engine.ts @@ -14,6 +14,7 @@ import { PaginateTag } from './tags/paginate-tag' import { RenderTag, IncludeTag } from './tags/render-tag' import { StyleTag, StylesheetTag } from './tags/style-tag' import { JavaScriptTag } from './tags/javascript-tag' +import { FormTag } from './tags/form-tag' interface EngineCache { [templatePath: string]: TemplateCache @@ -112,6 +113,7 @@ class LiquidEngine { this.liquid.registerTag('stylesheet', StylesheetTag) this.liquid.registerTag('script', ScriptTag) this.liquid.registerTag('javascript', JavaScriptTag) + this.liquid.registerTag('form', FormTag) } /** diff --git a/lib/store-renderer/liquid/tags/form-tag.ts b/lib/store-renderer/liquid/tags/form-tag.ts new file mode 100644 index 00000000..c1c54993 --- /dev/null +++ b/lib/store-renderer/liquid/tags/form-tag.ts @@ -0,0 +1,230 @@ +import { + TagToken, + Context, + Emitter, + Tag, + Template, + TopLevelToken, + TokenKind, + Liquid, +} from 'liquidjs' + +/** + * Tag para manejar formularios de Shopify + * Sintaxis: {% form 'type', object %}...{% endform %} + * + * Tipos soportados: + * - 'contact' : Formulario de contacto + * - 'newsletter' : Formulario de newsletter + * - 'product' : Formulario de producto (add to cart) + * - 'login' : Formulario de login + * - 'register' : Formulario de registro + * - 'recover_password' : Formulario de recuperar contraseña + */ +export class FormTag extends Tag { + private formType: string = '' + private formObject: any = null + private formAttributes: Record = {} + private templateContent: string = '' + + constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) { + super(tagToken, remainTokens, liquid) + + // Parsear argumentos del tag + this.parseArguments(tagToken.args) + + // Parsear contenido hasta endform + this.parseTemplateContent(remainTokens) + } + + /** + * Parsea el contenido del template hasta encontrar endform + */ + private parseTemplateContent(remainTokens: TopLevelToken[]): void { + const contentTokens: string[] = [] + let closed = false + + while (remainTokens.length) { + const token = remainTokens.shift() + if (!token) break + + if (token.kind === TokenKind.Tag && (token as any).name === 'endform') { + closed = true + break + } + + if (token.kind === TokenKind.HTML) { + // Acceder correctamente al contenido HTML + const htmlToken = token as any + const tokenContent = htmlToken.input + ? htmlToken.input.substring(htmlToken.begin, htmlToken.end) + : '' + contentTokens.push(tokenContent) + } else if (token.kind === TokenKind.Output) { + const outputToken = token as any + const tokenContent = outputToken.content || outputToken.value || '' + contentTokens.push(`{{ ${tokenContent} }}`) + } else if (token.kind === TokenKind.Tag) { + const tagToken = token as any + const tokenContent = tagToken.content || tagToken.value || '' + contentTokens.push(`{% ${tokenContent} %}`) + } + } + + if (!closed) { + throw new Error('tag {% form %} not closed') + } + + this.templateContent = contentTokens.join('') + } + + /** + * Parsea los argumentos del tag form + */ + private parseArguments(args: string): void { + // Remover espacios y dividir argumentos + const cleanArgs = args.trim() + + // Ejemplo: 'contact', object, class: 'my-form' + const parts = cleanArgs.split(',').map(part => part.trim()) + + if (parts.length > 0) { + // El primer argumento es el tipo de formulario (entre comillas) + this.formType = parts[0].replace(/['"`]/g, '') + } + + if (parts.length > 1) { + // El segundo argumento puede ser un objeto + this.formObject = parts[1] + } + + // Parsear atributos adicionales (class, id, etc.) + for (let i = 2; i < parts.length; i++) { + const attr = parts[i] + const [key, value] = attr.split(':').map(s => s.trim()) + if (key && value) { + this.formAttributes[key] = value.replace(/['"`]/g, '') + } + } + } + + /** + * Renderiza el tag form + */ + *render(ctx: Context, emitter: Emitter): Generator { + const formAction = this.getFormAction(this.formType) + const formMethod = this.getFormMethod(this.formType) + const formClass = this.getFormClass(this.formType) + const formId = this.getFormId(this.formType) + + // Construir atributos del formulario + let attributes = `action="${formAction}" method="${formMethod}"` + + if (formClass) { + attributes += ` class="${formClass}"` + } + + if (formId) { + attributes += ` id="${formId}"` + } + + // Agregar atributos personalizados + Object.entries(this.formAttributes).forEach(([key, value]) => { + attributes += ` ${key}="${value}"` + }) + + // Abrir el formulario + emitter.write(`
`) + + // Agregar campos ocultos necesarios + this.addHiddenFields(emitter, this.formType) + + // Renderizar contenido interno del formulario + // Por ahora renderizamos el contenido como texto simple + // TODO: Procesar el contenido Liquid correctamente + emitter.write(this.templateContent) + + // Cerrar el formulario + emitter.write('
') + } + + /** + * Obtiene la acción del formulario según el tipo + */ + private getFormAction(type: string): string { + const actions: Record = { + contact: '/contact', + newsletter: '/newsletter', + product: '/cart/add', + login: '/account/login', + register: '/account/register', + recover_password: '/account/recover', + customer: '/customer', + storefront_password: '/password', + } + + return actions[type] || '/contact' + } + + /** + * Obtiene el método HTTP del formulario + */ + private getFormMethod(type: string): string { + // La mayoría de formularios de Shopify usan POST + return 'post' + } + + /** + * Obtiene la clase CSS del formulario + */ + private getFormClass(type: string): string { + const classes: Record = { + contact: 'contact-form', + newsletter: 'newsletter-form', + product: 'product-form', + login: 'login-form', + register: 'register-form', + recover_password: 'recover-form', + customer: 'customer-form', + } + + return classes[type] || 'form' + } + + /** + * Obtiene el ID del formulario + */ + private getFormId(type: string): string { + return `${type}-form` + } + + /** + * Agrega campos ocultos necesarios según el tipo de formulario + */ + private addHiddenFields(emitter: Emitter, type: string): void { + // Token CSRF (simulado para desarrollo) + emitter.write('') + emitter.write('') + + // Campos específicos por tipo + switch (type) { + case 'contact': + emitter.write('') + break + + case 'newsletter': + emitter.write('') + break + + case 'product': + emitter.write('') + break + + case 'login': + emitter.write('') + break + } + } +} + +// EndFormTag no es necesario ya que FormTag se encarga de abrir y cerrar el formulario diff --git a/lib/store-renderer/services/fetchers/collection-fetcher.ts b/lib/store-renderer/services/fetchers/collection-fetcher.ts index 7b6d2237..70097475 100644 --- a/lib/store-renderer/services/fetchers/collection-fetcher.ts +++ b/lib/store-renderer/services/fetchers/collection-fetcher.ts @@ -127,7 +127,9 @@ export class CollectionFetcher { * Transforma una colección de Amplify al formato Liquid */ private async transformCollection(collection: any, storeId: string): Promise { - const handle = dataTransformer.createHandle(`collection-${collection.id}`) + const handle = dataTransformer.createHandle( + collection.name || collection.title || `collection-${collection.id}` + ) // Obtener productos de la colección si existe relación const products: ProductContext[] = [] diff --git a/lib/store-renderer/services/rendering/section-renderer.ts b/lib/store-renderer/services/rendering/section-renderer.ts index 96466a20..2f505a61 100644 --- a/lib/store-renderer/services/rendering/section-renderer.ts +++ b/lib/store-renderer/services/rendering/section-renderer.ts @@ -13,10 +13,9 @@ export class SectionRenderer { baseContext: RenderContext, storeTemplate?: any ): Promise { - try { - // Extraer settings del schema como fallback - const schemaSettings = schemaParser.extractSchemaSettings(templateContent) + const schemaSettings = schemaParser.extractSchemaSettings(templateContent) + try { // Obtener settings y blocks reales del storeTemplate si existe const storeSection = storeTemplate?.sections?.[sectionName] const actualSettings = storeSection?.settings || {} @@ -39,7 +38,33 @@ export class SectionRenderer { return await liquidEngine.render(templateContent, sectionContext, `section_${sectionName}`) } catch (error) { console.error(`Error rendering section ${sectionName}:`, error) - return `` + + // Intentar render con contexto más simple como fallback + if (error instanceof Error && error.message.includes('unexpected')) { + console.warn(`Attempting simplified render for section ${sectionName}...`) + try { + // Contexto más básico sin blocks complejos + const simpleContext = { + ...baseContext, + section: { + id: sectionName, + settings: schemaSettings, // Solo usar defaults del schema + }, + } + + return await liquidEngine.render( + templateContent, + simpleContext, + `section_${sectionName}_simple` + ) + } catch (fallbackError) { + console.error(`Simplified render also failed for ${sectionName}:`, fallbackError) + } + } + + // Si todo falla, mostrar placeholder informativo + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + return `` } } diff --git a/lib/store-renderer/services/templates/schema-parser.ts b/lib/store-renderer/services/templates/schema-parser.ts index eb56a0d6..a152a469 100644 --- a/lib/store-renderer/services/templates/schema-parser.ts +++ b/lib/store-renderer/services/templates/schema-parser.ts @@ -1,4 +1,86 @@ export class SchemaParser { + /** + * Limpia y valida el contenido JSON de un schema + */ + private cleanSchemaJSON(jsonContent: string): string { + try { + // Remover comentarios tipo // (no válidos en JSON) + let cleaned = jsonContent.replace(/\/\/.*$/gm, '') + + // Remover comentarios tipo /* */ (no válidos en JSON) + cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '') + + // Arreglar comas finales antes de } o ] + cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1') + + // Arreglar comas dobles + cleaned = cleaned.replace(/,,+/g, ',') + + // Remover espacios extra y saltos de línea extra + cleaned = cleaned.replace(/\s+/g, ' ').trim() + + // Intentar validar brackets (pero no fallar si hay problemas) + try { + this.validateBracketsBalance(cleaned) + } catch (bracketError) { + const errorMessage = + bracketError instanceof Error ? bracketError.message : 'Unknown bracket error' + console.warn('Schema has unbalanced brackets, but continuing with parsing:', errorMessage) + // No retornamos error, intentamos parsear el JSON de todas formas + } + + return cleaned + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + console.warn('Error cleaning schema JSON, using original:', errorMessage) + // Si hay error en la limpieza, devolver el contenido original + return jsonContent + } + } + + /** + * Valida que los brackets estén balanceados en el JSON + */ + private validateBracketsBalance(jsonContent: string): void { + let braceCount = 0 + let bracketCount = 0 + let inString = false + let escapeNext = false + + for (let i = 0; i < jsonContent.length; i++) { + const char = jsonContent[i] + + if (escapeNext) { + escapeNext = false + continue + } + + if (char === '\\') { + escapeNext = true + continue + } + + if (char === '"' && !escapeNext) { + inString = !inString + continue + } + + if (!inString) { + if (char === '{') braceCount++ + else if (char === '}') braceCount-- + else if (char === '[') bracketCount++ + else if (char === ']') bracketCount-- + } + } + + if (braceCount !== 0) { + throw new Error(`Unbalanced braces: ${braceCount > 0 ? 'missing }' : 'extra }'}`) + } + if (bracketCount !== 0) { + throw new Error(`Unbalanced brackets: ${bracketCount > 0 ? 'missing ]' : 'extra ]'}`) + } + } + /** * Extrae los settings del schema de un template usando expresiones regulares */ @@ -12,8 +94,32 @@ export class SchemaParser { return {} } - // Parsear el JSON del schema - const schemaJSON = JSON.parse(match[1].trim()) + // Limpiar el contenido del schema antes de parsear + const rawSchemaContent = match[1].trim() + + // Intentar parsear directamente primero + let schemaJSON: any + try { + schemaJSON = JSON.parse(rawSchemaContent) + } catch (directParseError) { + // Si falla el parseo directo, intentar con limpieza + try { + const cleanedSchemaContent = this.cleanSchemaJSON(rawSchemaContent) + + // Log para debug solo en desarrollo + if (process.env.NODE_ENV === 'development') { + console.log( + 'Schema content to parse (first 200 chars):', + cleanedSchemaContent.substring(0, 200) + '...' + ) + } + + schemaJSON = JSON.parse(cleanedSchemaContent) + } catch (cleanParseError) { + console.warn('Failed to parse schema JSON after cleaning, skipping schema settings') + return {} + } + } if (!schemaJSON.settings) { return {} @@ -30,7 +136,15 @@ export class SchemaParser { return settings } catch (error) { - console.warn('Error extracting schema settings:', error) + console.error('Error extracting schema settings:', error) + + // Buscar nuevamente el match para el log de error + const schemaRegex = /{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/i + const errorMatch = templateContent.match(schemaRegex) + if (errorMatch?.[1]) { + console.error('Schema content that failed:', errorMatch[1].substring(0, 500) + '...') + } + return {} } } @@ -47,7 +161,23 @@ export class SchemaParser { return [] } - const schemaJSON = JSON.parse(match[1].trim()) + const rawContent = match[1].trim() + + // Intentar parseo directo primero + let schemaJSON: any + try { + schemaJSON = JSON.parse(rawContent) + } catch (directError) { + // Intentar con limpieza + try { + const cleanedContent = this.cleanSchemaJSON(rawContent) + schemaJSON = JSON.parse(cleanedContent) + } catch (cleanError) { + console.warn('Error extracting schema blocks:', cleanError) + return [] + } + } + return schemaJSON.blocks || [] } catch (error) { console.warn('Error extracting schema blocks:', error) @@ -97,7 +227,19 @@ export class SchemaParser { return {} } - return JSON.parse(match[1].trim()) + const rawContent = match[1].trim() + + try { + return JSON.parse(rawContent) + } catch (directError) { + try { + const cleanedContent = this.cleanSchemaJSON(rawContent) + return JSON.parse(cleanedContent) + } catch (cleanError) { + console.warn('Error extracting full schema:', cleanError) + return {} + } + } } catch (error) { console.warn('Error extracting schema:', error) return {} From c9052df151a19864735f6b269fc165e6504ab140 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Mon, 9 Jun 2025 12:05:15 -0500 Subject: [PATCH 3/5] chore(route): remove deprecated store rendering API endpoint and associated functions --- app/api/stores/render/route.ts | 152 --------------------------------- 1 file changed, 152 deletions(-) delete mode 100644 app/api/stores/render/route.ts diff --git a/app/api/stores/render/route.ts b/app/api/stores/render/route.ts deleted file mode 100644 index 5dfa0bae..00000000 --- a/app/api/stores/render/route.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { storeRenderer } from '@/lib/store-renderer' - -/** - * API endpoint para renderizar páginas de tiendas - * - * GET /api/stores/render?domain=example.com&path=/products/mi-producto - * - * Query params: - * - domain: Dominio de la tienda (requerido) - * - path: Path de la página a renderizar (opcional, default: '/') - */ -export async function GET(request: NextRequest) { - try { - const searchParams = request.nextUrl.searchParams - const domain = searchParams.get('domain') - const path = searchParams.get('path') || '/' - - // Validar parámetros requeridos - if (!domain) { - return NextResponse.json({ error: 'Domain parameter is required' }, { status: 400 }) - } - - // Validar formato del dominio - if (!isValidDomain(domain)) { - return NextResponse.json({ error: 'Invalid domain format' }, { status: 400 }) - } - - // Renderizar página usando el sistema de renderizado - const result = await storeRenderer.renderPage(domain, path) - - // Configurar headers de respuesta - const headers = new Headers({ - 'Content-Type': 'text/html; charset=utf-8', - 'Cache-Control': `public, max-age=${Math.floor(result.cacheTTL / 1000)}`, - 'X-Store-Cache-Key': result.cacheKey, - }) - - // Añadir headers SEO si existen - if (result.metadata.canonical) { - headers.set('Link', `<${result.metadata.canonical}>; rel="canonical"`) - } - - // Crear respuesta HTML con metadata incluida - const fullHtml = generateFullHTML(result.html, result.metadata) - - return new NextResponse(fullHtml, { - status: 200, - headers, - }) - } catch (error: any) { - console.error('Error rendering store page:', error) - - // Manejar errores tipados del sistema de renderizado - if (error.type && error.statusCode) { - return NextResponse.json( - { - error: error.message, - type: error.type, - details: process.env.APP_ENV === 'development' ? error.details : undefined, - }, - { status: error.statusCode } - ) - } - - // Error genérico - return NextResponse.json( - { - error: 'Internal server error', - message: process.env.APP_ENV === 'development' ? error.message : 'Something went wrong', - }, - { status: 500 } - ) - } -} - -/** - * Valida si un dominio tiene formato correcto - */ -function isValidDomain(domain: string): boolean { - const domainRegex = - /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.([a-zA-Z]{2,}|[a-zA-Z]{2,}\.[a-zA-Z]{2,})$/ - return domainRegex.test(domain) || domain.includes('.fasttify.com') -} - -/** - * Genera HTML completo con metadata SEO - */ -function generateFullHTML(body: string, metadata: any): string { - const { title, description, canonical, openGraph, schema } = metadata - - return ` - - - - - - - ${escapeHtml(title)} - - ${canonical ? `` : ''} - - - ${openGraph ? generateOpenGraphTags(openGraph) : ''} - - - ${schema ? `` : ''} - - - - - - - ${body} - - -` -} - -/** - * Genera tags de Open Graph - */ -function generateOpenGraphTags(og: any): string { - return ` - - - - - ${og.image ? `` : ''} - ${og.site_name ? `` : ''} - - - - - - ${og.image ? `` : ''} - `.trim() -} - -/** - * Escapa HTML para prevenir XSS - */ -function escapeHtml(text: string): string { - if (!text) return '' - - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} From 912849039f2a4b8820c7a4b12d77b3ecaed3def5 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Mon, 9 Jun 2025 12:32:03 -0500 Subject: [PATCH 4/5] refactor(store-renderer): update import paths to use absolute references; enhance cache management in Liquid engine and related services --- lib/store-renderer/index.ts | 19 +-- lib/store-renderer/liquid/engine.ts | 75 ++++------ lib/store-renderer/liquid/filters.ts | 2 +- lib/store-renderer/renderers/homepage.ts | 16 +-- .../services/core/cache-manager.ts | 18 +++ .../services/core/domain-resolver.ts | 136 +++--------------- .../services/fetchers/collection-fetcher.ts | 6 +- .../services/fetchers/data-fetcher.ts | 10 +- .../services/fetchers/product-fetcher.ts | 6 +- .../services/fetchers/template-fetcher.ts | 4 +- .../services/rendering/context-builder.ts | 4 +- .../services/rendering/metadata-generator.ts | 2 +- .../services/rendering/section-renderer.ts | 8 +- .../services/templates/template-loader.ts | 107 ++++---------- 14 files changed, 133 insertions(+), 280 deletions(-) diff --git a/lib/store-renderer/index.ts b/lib/store-renderer/index.ts index d34934a3..d2b5adcd 100644 --- a/lib/store-renderer/index.ts +++ b/lib/store-renderer/index.ts @@ -1,5 +1,8 @@ -import { DynamicPageRenderer, type PageRenderOptions } from './renderers/homepage' -import type { RenderResult } from './types' +import { + DynamicPageRenderer, + type PageRenderOptions, +} from '@/lib/store-renderer/renderers/homepage' +import type { RenderResult } from '@/lib/store-renderer/types' /** * Factory principal del sistema de renderizado de tiendas @@ -134,11 +137,11 @@ export class StoreRendererFactory { export const storeRenderer = new StoreRendererFactory() // Exportar tipos para uso externo -export type { RenderResult } from './types' -export { DynamicPageRenderer } from './renderers/homepage' +export type { RenderResult } from '@/lib/store-renderer/types' +export { DynamicPageRenderer } from '@/lib/store-renderer/renderers/homepage' // Exportar servicios para uso avanzado -export { domainResolver } from './services/core/domain-resolver' -export { templateLoader } from './services/templates/template-loader' -export { dataFetcher } from './services/fetchers/data-fetcher' -export { liquidEngine } from './liquid/engine' +export { domainResolver } from '@/lib/store-renderer/services/core/domain-resolver' +export { templateLoader } from '@/lib/store-renderer/services/templates/template-loader' +export { dataFetcher } from '@/lib/store-renderer/services/fetchers/data-fetcher' +export { liquidEngine } from '@/lib/store-renderer/liquid/engine' diff --git a/lib/store-renderer/liquid/engine.ts b/lib/store-renderer/liquid/engine.ts index 282a9806..17b0cfac 100644 --- a/lib/store-renderer/liquid/engine.ts +++ b/lib/store-renderer/liquid/engine.ts @@ -5,26 +5,21 @@ import type { TemplateCache, LiquidContext, TemplateError, -} from '../types' -import { ecommerceFilters } from './filters' -import { SchemaTag } from './tags/schema-tag' -import { ScriptTag } from './tags/script-tag' -import { SectionTag } from './tags/section-tag' -import { PaginateTag } from './tags/paginate-tag' -import { RenderTag, IncludeTag } from './tags/render-tag' -import { StyleTag, StylesheetTag } from './tags/style-tag' -import { JavaScriptTag } from './tags/javascript-tag' -import { FormTag } from './tags/form-tag' - -interface EngineCache { - [templatePath: string]: TemplateCache -} +} from '@/lib/store-renderer/types' +import { ecommerceFilters } from '@/lib/store-renderer/liquid/filters' +import { SchemaTag } from '@/lib/store-renderer/liquid/tags/schema-tag' +import { ScriptTag } from '@/lib/store-renderer/liquid/tags/script-tag' +import { SectionTag } from '@/lib/store-renderer/liquid/tags/section-tag' +import { PaginateTag } from '@/lib/store-renderer/liquid/tags/paginate-tag' +import { RenderTag, IncludeTag } from '@/lib/store-renderer/liquid/tags/render-tag' +import { StyleTag, StylesheetTag } from '@/lib/store-renderer/liquid/tags/style-tag' +import { JavaScriptTag } from '@/lib/store-renderer/liquid/tags/javascript-tag' +import { FormTag } from '@/lib/store-renderer/liquid/tags/form-tag' +import { cacheManager } from '@/lib/store-renderer/services/core/cache-manager' class LiquidEngine { private static instance: LiquidEngine private liquid: Liquid - private cache: EngineCache = {} - private readonly TEMPLATE_CACHE_TTL = 60 * 60 * 1000 // 1 hora en ms private constructor() { this.liquid = this.createEngine() @@ -217,21 +212,16 @@ class LiquidEngine { * Obtiene una plantilla del caché si existe y es válida */ private getCachedTemplate(templatePath: string, content: string): any | null { - const cached = this.cache[templatePath] - if (!cached) { - return null - } + const cacheKey = `template_${templatePath}` + const cached = cacheManager.getCached(cacheKey) as TemplateCache | null - const now = Date.now() - if (now > cached.lastUpdated.getTime() + cached.ttl) { - // Caché expirado - delete this.cache[templatePath] + if (!cached) { return null } // Verificar que el contenido no haya cambiado if (cached.content !== content) { - delete this.cache[templatePath] + cacheManager.invalidateTemplateCache(templatePath) return null } @@ -242,12 +232,15 @@ class LiquidEngine { * Guarda una plantilla compilada en caché */ private setCachedTemplate(templatePath: string, content: string, compiled: any): void { - this.cache[templatePath] = { + const cacheKey = `template_${templatePath}` + const templateCache: TemplateCache = { content, compiledTemplate: compiled, lastUpdated: new Date(), - ttl: this.TEMPLATE_CACHE_TTL, + ttl: cacheManager.TEMPLATE_CACHE_TTL, } + + cacheManager.setCached(cacheKey, templateCache, cacheManager.TEMPLATE_CACHE_TTL) } /** @@ -255,14 +248,14 @@ class LiquidEngine { * @param templatePath - Path de la plantilla a invalidar */ public invalidateCache(templatePath: string): void { - delete this.cache[templatePath] + cacheManager.invalidateTemplateCache(templatePath) } /** * Limpia todo el caché de plantillas */ public clearCache(): void { - this.cache = {} + cacheManager.clearCache() // Recrear la instancia de Liquid para limpiar su caché interno this.liquid = this.createEngine() this.registerFilters() @@ -272,34 +265,14 @@ class LiquidEngine { * Limpia plantillas expiradas del caché */ public cleanExpiredCache(): void { - const now = Date.now() - Object.keys(this.cache).forEach(templatePath => { - const cached = this.cache[templatePath] - if (now > cached.lastUpdated.getTime() + cached.ttl) { - delete this.cache[templatePath] - } - }) + cacheManager.cleanExpiredCache() } /** * Obtiene estadísticas del caché para debugging */ public getCacheStats(): { total: number; expired: number; active: number } { - const now = Date.now() - let total = 0 - let expired = 0 - let active = 0 - - Object.values(this.cache).forEach(cached => { - total++ - if (now > cached.lastUpdated.getTime() + cached.ttl) { - expired++ - } else { - active++ - } - }) - - return { total, expired, active } + return cacheManager.getCacheStats() } /** diff --git a/lib/store-renderer/liquid/filters.ts b/lib/store-renderer/liquid/filters.ts index 4645b1a8..6e5f513c 100644 --- a/lib/store-renderer/liquid/filters.ts +++ b/lib/store-renderer/liquid/filters.ts @@ -1,4 +1,4 @@ -import type { LiquidFilter } from '../types' +import type { LiquidFilter } from '@/lib/store-renderer/types' /** * Filtro para formatear precios con moneda diff --git a/lib/store-renderer/renderers/homepage.ts b/lib/store-renderer/renderers/homepage.ts index 54190270..42ea05e8 100644 --- a/lib/store-renderer/renderers/homepage.ts +++ b/lib/store-renderer/renderers/homepage.ts @@ -1,11 +1,11 @@ -import { domainResolver } from '../services/core/domain-resolver' -import { templateLoader } from '../services/templates/template-loader' -import { dataFetcher } from '../services/fetchers/data-fetcher' -import { liquidEngine } from '../liquid/engine' -import { contextBuilder } from '../services/rendering/context-builder' -import { metadataGenerator } from '../services/rendering/metadata-generator' -import { sectionRenderer } from '../services/rendering/section-renderer' -import type { RenderResult, TemplateError } from '../types' +import { domainResolver } from '@/lib/store-renderer/services/core/domain-resolver' +import { templateLoader } from '@/lib/store-renderer/services/templates/template-loader' +import { dataFetcher } from '@/lib/store-renderer/services/fetchers/data-fetcher' +import { liquidEngine } from '@/lib/store-renderer/liquid/engine' +import { contextBuilder } from '@/lib/store-renderer/services/rendering/context-builder' +import { metadataGenerator } from '@/lib/store-renderer/services/rendering/metadata-generator' +import { sectionRenderer } from '@/lib/store-renderer/services/rendering/section-renderer' +import type { RenderResult, TemplateError } from '@/lib/store-renderer/types' export interface PageRenderOptions { pageType: diff --git a/lib/store-renderer/services/core/cache-manager.ts b/lib/store-renderer/services/core/cache-manager.ts index 411e5536..27490621 100644 --- a/lib/store-renderer/services/core/cache-manager.ts +++ b/lib/store-renderer/services/core/cache-manager.ts @@ -14,6 +14,8 @@ export class CacheManager { public readonly PRODUCT_CACHE_TTL = 15 * 60 * 1000 // 15 minutos public readonly COLLECTION_CACHE_TTL = 30 * 60 * 1000 // 30 minutos public readonly STORE_CACHE_TTL = 30 * 60 * 1000 // 30 minutos + public readonly DOMAIN_CACHE_TTL = 30 * 60 * 1000 // 30 minutos + public readonly TEMPLATE_CACHE_TTL = 60 * 60 * 1000 // 1 hora private constructor() {} @@ -83,6 +85,22 @@ export class CacheManager { }) } + /** + * Invalida el caché para un dominio específico + */ + public invalidateDomainCache(domain: string): void { + const key = `domain_${domain}` + delete this.cache[key] + } + + /** + * Invalida el caché para un template específico + */ + public invalidateTemplateCache(templatePath: string): void { + const key = `template_${templatePath}` + delete this.cache[key] + } + /** * Limpia todo el caché */ diff --git a/lib/store-renderer/services/core/domain-resolver.ts b/lib/store-renderer/services/core/domain-resolver.ts index 3b5f5b55..2d2c67e0 100644 --- a/lib/store-renderer/services/core/domain-resolver.ts +++ b/lib/store-renderer/services/core/domain-resolver.ts @@ -1,18 +1,9 @@ import { cookiesClient } from '@/utils/AmplifyServer' -import type { DomainResolution, Store, TemplateError } from '../../types' - -interface DomainCache { - [domain: string]: { - data: DomainResolution | null - timestamp: number - ttl: number - } -} +import type { DomainResolution, Store, TemplateError } from '@/lib/store-renderer/types' +import { cacheManager } from '@/lib/store-renderer/services/core/cache-manager' class DomainResolver { private static instance: DomainResolver - private cache: DomainCache = {} - private readonly CACHE_TTL = 30 * 60 * 1000 // 30 minutos en ms private constructor() {} @@ -24,15 +15,16 @@ class DomainResolver { } /** - * Resuelve un dominio a información de tienda + * Resuelve un dominio a información completa de tienda * @param domain - El dominio completo (ej: "usuario.fasttify.com") - * @returns DomainResolution o null si no se encuentra + * @returns Store completa o null si no se encuentra */ - public async resolveDomain(domain: string): Promise { + public async resolveDomain(domain: string): Promise { try { // Verificar caché primero - const cached = this.getCached(domain) - if (cached !== undefined) { + const cacheKey = `domain_${domain}` + const cached = cacheManager.getCached(cacheKey) + if (cached !== null) { return cached } @@ -43,21 +35,15 @@ class DomainResolver { if (!stores || stores.length === 0) { // Cachear resultado negativo por menos tiempo (5 minutos) - this.setCached(domain, null, 5 * 60 * 1000) + cacheManager.setCached(cacheKey, null, 5 * 60 * 1000) return null } - const store = stores[0] // Debería ser único por dominio - const resolution: DomainResolution = { - storeId: store.storeId, - storeName: store.storeName, - customDomain: store.customDomain || '', - isActive: store.onboardingCompleted && store.storeStatus !== 'inactive', - } + const store = stores[0] as Store // Debería ser único por dominio // Cachear resultado positivo - this.setCached(domain, resolution, this.CACHE_TTL) - return resolution + cacheManager.setCached(cacheKey, store, cacheManager.DOMAIN_CACHE_TTL) + return store } catch (error) { console.error(`Error resolving domain ${domain}:`, error) @@ -65,38 +51,15 @@ class DomainResolver { } } - /** - * Obtiene la información completa de la tienda por storeId - * @param storeId - ID de la tienda - * @returns Store o null si no se encuentra - */ - public async getStoreById(storeId: string): Promise { - try { - const { data: store } = await cookiesClient.models.UserStore.get({ - storeId: storeId, - }) - - if (!store) { - return null - } - - return store as Store - } catch (error) { - console.error(`Error fetching store ${storeId}:`, error) - - return null - } - } - /** * Resuelve un dominio completo: busca y retorna información completa de la tienda * @param domain - El dominio completo * @returns Store completa o lanza error */ public async resolveStoreByDomain(domain: string): Promise { - const resolution = await this.resolveDomain(domain) + const store = await this.resolveDomain(domain) - if (!resolution) { + if (!store) { const error: TemplateError = { type: 'STORE_NOT_FOUND', message: `No store found for domain: ${domain}`, @@ -105,7 +68,8 @@ class DomainResolver { throw error } - if (!resolution.isActive) { + const isActive = store.onboardingCompleted && store.storeStatus !== 'inactive' + if (!isActive) { const error: TemplateError = { type: 'STORE_NOT_FOUND', message: `Store is not active for domain: ${domain}`, @@ -114,17 +78,6 @@ class DomainResolver { throw error } - const store = await this.getStoreById(resolution.storeId) - - if (!store) { - const error: TemplateError = { - type: 'DATA_ERROR', - message: `Store data not found for ID: ${resolution.storeId}`, - statusCode: 500, - } - throw error - } - return store } @@ -133,77 +86,28 @@ class DomainResolver { * @param domain - Dominio a invalidar */ public invalidateCache(domain: string): void { - delete this.cache[domain] + cacheManager.invalidateDomainCache(domain) } /** * Limpia todo el caché */ public clearCache(): void { - this.cache = {} + cacheManager.clearCache() } /** * Limpia entradas expiradas del caché */ public cleanExpiredCache(): void { - const now = Date.now() - Object.keys(this.cache).forEach(domain => { - const entry = this.cache[domain] - if (now > entry.timestamp + entry.ttl) { - delete this.cache[domain] - } - }) - } - - /** - * Obtiene una entrada del caché si existe y no ha expirado - */ - private getCached(domain: string): DomainResolution | null | undefined { - const entry = this.cache[domain] - if (!entry) { - return undefined - } - - const now = Date.now() - if (now > entry.timestamp + entry.ttl) { - delete this.cache[domain] - return undefined - } - - return entry.data - } - - /** - * Guarda una entrada en el caché - */ - private setCached(domain: string, data: DomainResolution | null, ttl: number): void { - this.cache[domain] = { - data, - timestamp: Date.now(), - ttl, - } + cacheManager.cleanExpiredCache() } /** * Obtiene estadísticas del caché para debugging */ public getCacheStats(): { total: number; expired: number; active: number } { - const now = Date.now() - let total = 0 - let expired = 0 - let active = 0 - - Object.values(this.cache).forEach(entry => { - total++ - if (now > entry.timestamp + entry.ttl) { - expired++ - } else { - active++ - } - }) - - return { total, expired, active } + return cacheManager.getCacheStats() } } diff --git a/lib/store-renderer/services/fetchers/collection-fetcher.ts b/lib/store-renderer/services/fetchers/collection-fetcher.ts index 70097475..ef28600e 100644 --- a/lib/store-renderer/services/fetchers/collection-fetcher.ts +++ b/lib/store-renderer/services/fetchers/collection-fetcher.ts @@ -1,7 +1,7 @@ import { cookiesClient } from '@/utils/AmplifyServer' -import { cacheManager } from '../core/cache-manager' -import { dataTransformer } from '../core/data-transformer' -import type { CollectionContext, ProductContext, TemplateError } from '../../types' +import { cacheManager } from '@/lib/store-renderer/services/core/cache-manager' +import { dataTransformer } from '@/lib/store-renderer/services/core/data-transformer' +import type { CollectionContext, ProductContext, TemplateError } from '@/lib/store-renderer/types' interface PaginationOptions { limit?: number diff --git a/lib/store-renderer/services/fetchers/data-fetcher.ts b/lib/store-renderer/services/fetchers/data-fetcher.ts index 6fcb1262..44733b83 100644 --- a/lib/store-renderer/services/fetchers/data-fetcher.ts +++ b/lib/store-renderer/services/fetchers/data-fetcher.ts @@ -1,8 +1,8 @@ -import { cacheManager } from '../core/cache-manager' -import { productFetcher } from '../fetchers/product-fetcher' -import { collectionFetcher } from '../fetchers/collection-fetcher' -import { templateFetcher } from '../fetchers/template-fetcher' -import type { ProductContext, CollectionContext } from '../../types' +import { cacheManager } from '@/lib/store-renderer/services/core/cache-manager' +import { productFetcher } from '@/lib/store-renderer/services/fetchers/product-fetcher' +import { collectionFetcher } from '@/lib/store-renderer/services/fetchers/collection-fetcher' +import { templateFetcher } from '@/lib/store-renderer/services/fetchers/template-fetcher' +import type { ProductContext, CollectionContext } from '@/lib/store-renderer/types' interface PaginationOptions { limit?: number diff --git a/lib/store-renderer/services/fetchers/product-fetcher.ts b/lib/store-renderer/services/fetchers/product-fetcher.ts index 04cdfbab..1415ae01 100644 --- a/lib/store-renderer/services/fetchers/product-fetcher.ts +++ b/lib/store-renderer/services/fetchers/product-fetcher.ts @@ -1,7 +1,7 @@ import { cookiesClient } from '@/utils/AmplifyServer' -import { cacheManager } from '../core/cache-manager' -import { dataTransformer } from '../core/data-transformer' -import type { ProductContext, TemplateError } from '../../types' +import { cacheManager } from '@/lib/store-renderer/services/core/cache-manager' +import { dataTransformer } from '@/lib/store-renderer/services/core/data-transformer' +import type { ProductContext, TemplateError } from '@/lib/store-renderer/types' interface PaginationOptions { limit?: number diff --git a/lib/store-renderer/services/fetchers/template-fetcher.ts b/lib/store-renderer/services/fetchers/template-fetcher.ts index 8193a58e..2139e7c9 100644 --- a/lib/store-renderer/services/fetchers/template-fetcher.ts +++ b/lib/store-renderer/services/fetchers/template-fetcher.ts @@ -1,6 +1,6 @@ import { cookiesClient } from '@/utils/AmplifyServer' -import { cacheManager } from '../core/cache-manager' -import type { TemplateError } from '../../types' +import { cacheManager } from '@/lib/store-renderer/services/core/cache-manager' +import type { TemplateError } from '@/lib/store-renderer/types' export class TemplateFetcher { /** diff --git a/lib/store-renderer/services/rendering/context-builder.ts b/lib/store-renderer/services/rendering/context-builder.ts index 3e21c25f..a0631560 100644 --- a/lib/store-renderer/services/rendering/context-builder.ts +++ b/lib/store-renderer/services/rendering/context-builder.ts @@ -1,5 +1,5 @@ -import type { RenderContext, ShopContext, PageContext } from '../../types' -import { linkListService } from '../core/linkList-service' +import type { RenderContext, ShopContext, PageContext } from '@/lib/store-renderer/types' +import { linkListService } from '@/lib/store-renderer/services/core/linkList-service' export class ContextBuilder { /** diff --git a/lib/store-renderer/services/rendering/metadata-generator.ts b/lib/store-renderer/services/rendering/metadata-generator.ts index 48d8088f..be30314c 100644 --- a/lib/store-renderer/services/rendering/metadata-generator.ts +++ b/lib/store-renderer/services/rendering/metadata-generator.ts @@ -1,4 +1,4 @@ -import type { RenderResult, OpenGraphData, SchemaData } from '../../types' +import type { RenderResult, OpenGraphData, SchemaData } from '@/lib/store-renderer/types' export class MetadataGenerator { /** diff --git a/lib/store-renderer/services/rendering/section-renderer.ts b/lib/store-renderer/services/rendering/section-renderer.ts index 2f505a61..f191cbdf 100644 --- a/lib/store-renderer/services/rendering/section-renderer.ts +++ b/lib/store-renderer/services/rendering/section-renderer.ts @@ -1,7 +1,7 @@ -import { liquidEngine } from '../../liquid/engine' -import { templateLoader } from '../templates/template-loader' -import { schemaParser } from '../templates/schema-parser' -import type { RenderContext } from '../../types' +import { liquidEngine } from '@/lib/store-renderer/liquid/engine' +import { templateLoader } from '@/lib/store-renderer/services/templates/template-loader' +import { schemaParser } from '@/lib/store-renderer/services/templates/schema-parser' +import type { RenderContext } from '@/lib/store-renderer/types' export class SectionRenderer { /** diff --git a/lib/store-renderer/services/templates/template-loader.ts b/lib/store-renderer/services/templates/template-loader.ts index ecff1c6c..2e59e5d4 100644 --- a/lib/store-renderer/services/templates/template-loader.ts +++ b/lib/store-renderer/services/templates/template-loader.ts @@ -1,18 +1,11 @@ import { S3Client, GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3' -import type { TemplateFile, TemplateCache, TemplateError } from '../../types' +import type { TemplateFile, TemplateCache, TemplateError } from '@/lib/store-renderer/types' import { cookiesClient } from '@/utils/AmplifyServer' - -interface S3TemplateCache { - [storeId: string]: { - [templatePath: string]: TemplateCache - } -} +import { cacheManager } from '@/lib/store-renderer/services/core/cache-manager' class TemplateLoader { private static instance: TemplateLoader private s3Client?: S3Client - private cache: S3TemplateCache = {} - private readonly TEMPLATE_CACHE_TTL = 60 * 60 * 1000 // 1 hora en ms private readonly bucketName: string private readonly cloudFrontDomain: string private readonly appEnv: string @@ -198,8 +191,8 @@ class TemplateLoader { public async loadAsset(storeId: string, assetPath: string): Promise { try { // Verificar caché primero (para assets también) - const cacheKey = `assets/${assetPath}` - const cached = this.getCachedTemplate(storeId, cacheKey) + const cacheKey = `asset_${storeId}_${assetPath}` + const cached = cacheManager.getCached(cacheKey) as TemplateCache | null if (cached) { // Para assets, el contenido en caché es base64, convertir de vuelta a Buffer return Buffer.from(cached.content, 'base64') @@ -215,7 +208,12 @@ class TemplateLoader { } // Guardar en caché (convertir Buffer a base64 para almacenamiento) - this.setCachedTemplate(storeId, cacheKey, assetBuffer.toString('base64')) + const assetCache: TemplateCache = { + content: assetBuffer.toString('base64'), + lastUpdated: new Date(), + ttl: cacheManager.TEMPLATE_CACHE_TTL, + } + cacheManager.setCached(cacheKey, assetCache, cacheManager.TEMPLATE_CACHE_TTL) return assetBuffer } catch (error) { @@ -258,7 +256,7 @@ class TemplateLoader { * @param storeId - ID de la tienda */ public invalidateStoreCache(storeId: string): void { - delete this.cache[storeId] + cacheManager.invalidateStoreCache(storeId) } /** @@ -267,39 +265,22 @@ class TemplateLoader { * @param templatePath - Ruta de la plantilla */ public invalidateTemplateCache(storeId: string, templatePath: string): void { - if (this.cache[storeId]) { - delete this.cache[storeId][templatePath] - } + const cacheKey = `template_${storeId}_${templatePath}` + cacheManager.setCached(cacheKey, null, 0) // Invalidar estableciendo a null } /** * Limpia todo el caché */ public clearCache(): void { - this.cache = {} + cacheManager.clearCache() } /** * Limpia plantillas expiradas del caché */ public cleanExpiredCache(): void { - const now = Date.now() - - Object.keys(this.cache).forEach(storeId => { - const storeCache = this.cache[storeId] - - Object.keys(storeCache).forEach(templatePath => { - const cached = storeCache[templatePath] - if (now > cached.lastUpdated.getTime() + cached.ttl) { - delete storeCache[templatePath] - } - }) - - // Si no quedan plantillas en caché para esta tienda, eliminar la entrada - if (Object.keys(storeCache).length === 0) { - delete this.cache[storeId] - } - }) + cacheManager.cleanExpiredCache() } /** @@ -394,22 +375,8 @@ class TemplateLoader { * Obtiene una plantilla del caché si existe y es válida */ private getCachedTemplate(storeId: string, templatePath: string): TemplateCache | null { - const storeCache = this.cache[storeId] - if (!storeCache) { - return null - } - - const cached = storeCache[templatePath] - if (!cached) { - return null - } - - const now = Date.now() - if (now > cached.lastUpdated.getTime() + cached.ttl) { - delete storeCache[templatePath] - return null - } - + const cacheKey = `template_${storeId}_${templatePath}` + const cached = cacheManager.getCached(cacheKey) as TemplateCache | null return cached } @@ -417,15 +384,14 @@ class TemplateLoader { * Guarda una plantilla en caché */ private setCachedTemplate(storeId: string, templatePath: string, content: string): void { - if (!this.cache[storeId]) { - this.cache[storeId] = {} - } - - this.cache[storeId][templatePath] = { + const cacheKey = `template_${storeId}_${templatePath}` + const templateCache: TemplateCache = { content, lastUpdated: new Date(), - ttl: this.TEMPLATE_CACHE_TTL, + ttl: cacheManager.TEMPLATE_CACHE_TTL, } + + cacheManager.setCached(cacheKey, templateCache, cacheManager.TEMPLATE_CACHE_TTL) } /** @@ -456,26 +422,15 @@ class TemplateLoader { expiredTemplates: number activeTemplates: number } { - const now = Date.now() - let stores = 0 - let totalTemplates = 0 - let expiredTemplates = 0 - let activeTemplates = 0 - - Object.values(this.cache).forEach(storeCache => { - stores++ - - Object.values(storeCache).forEach(cached => { - totalTemplates++ - if (now > cached.lastUpdated.getTime() + cached.ttl) { - expiredTemplates++ - } else { - activeTemplates++ - } - }) - }) - - return { stores, totalTemplates, expiredTemplates, activeTemplates } + const globalStats = cacheManager.getCacheStats() + + // Para mantener compatibilidad, mapeamos las estadísticas globales + return { + stores: 0, // No podemos determinar esto fácilmente con el cache global + totalTemplates: globalStats.total, + expiredTemplates: globalStats.expired, + activeTemplates: globalStats.active, + } } } From bf85aaa99fd70da9aab5e12811267a4861dd5999 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Mon, 9 Jun 2025 12:50:21 -0500 Subject: [PATCH 5/5] feat(store-page): implement asset path validation and caching for page rendering; enhance metadata generation for static assets --- app/[store]/page.tsx | 57 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/app/[store]/page.tsx b/app/[store]/page.tsx index 7a196f44..a07e366f 100644 --- a/app/[store]/page.tsx +++ b/app/[store]/page.tsx @@ -1,10 +1,46 @@ import { Metadata } from 'next' import { notFound } from 'next/navigation' +import { cache } from 'react' import { storeRenderer } from '@/lib/store-renderer' // Forzar renderizado dinámico para acceder a variables de entorno en runtime export const dynamic = 'force-dynamic' +/** + * Verifica si el path corresponde a un asset estático + */ +function isAssetPath(path: string): boolean { + const assetExtensions = [ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.svg', + '.webp', + '.ico', + '.css', + '.js', + '.woff', + '.woff2', + '.ttf', + '.eot', + ] + return ( + assetExtensions.some(ext => path.toLowerCase().endsWith(ext)) || + path.startsWith('/assets/') || + path.startsWith('/_next/') || + path.includes('/icons/') + ) +} + +/** + * Función cacheada usando React.cache() que persiste entre generateMetadata y StorePage + * Esta es la forma oficial de Next.js para compartir datos entre estas funciones + */ +const getCachedRenderResult = cache(async (domain: string, path: string) => { + return await storeRenderer.renderPage(domain, path) +}) + interface StorePageProps { params: Promise<{ store: string @@ -24,12 +60,18 @@ export default async function StorePage({ params, searchParams }: StorePageProps const { store } = resolvedParams const path = resolvedSearchParams.path || '/' + // Validar que no sea una ruta de asset + if (isAssetPath(path)) { + console.warn(`[StorePage] Asset path ${path} should not be handled by page renderer`) + notFound() + } + try { // Resolver dominio completo (el middleware ya reescribió la URL) const domain = `${store}.fasttify.com` - // Renderizar página usando el sistema - const result = await storeRenderer.renderPage(domain, path) + // Renderizar página usando el sistema con caché temporal + const result = await getCachedRenderResult(domain, path) // Retornar HTML renderizado como componente dangerouslySetInnerHTML // Esto permite SSR completo con SEO optimizado @@ -59,10 +101,19 @@ export async function generateMetadata({ const { store } = resolvedParams const path = resolvedSearchParams.path || '/' + // No generar metadata para assets + if (isAssetPath(path)) { + return { + title: 'Asset', + description: 'Static asset', + } + } + try { const domain = `${store}.fasttify.com` - const result = await storeRenderer.renderPage(domain, path) + // Usar el cache global para obtener el resultado completo + const result = await getCachedRenderResult(domain, path) const { metadata } = result return {