diff --git a/amplify/functions/createStoreTemplate/config/defaultSections.json b/amplify/functions/createStoreTemplate/config/defaultSections.json
index bb5ddf04..85867d2b 100644
--- a/amplify/functions/createStoreTemplate/config/defaultSections.json
+++ b/amplify/functions/createStoreTemplate/config/defaultSections.json
@@ -110,47 +110,90 @@
"footer": {
"type": "footer",
"settings": {
- "background_color": "#1f2937",
- "text_color": "#f9fafb",
- "heading_color": "#ffffff",
- "link_color": "#d1d5db",
- "link_hover_color": "#ffffff",
- "show_social_links": true,
- "show_newsletter": true,
- "show_payment_icons": true,
- "show_store_info": true,
- "newsletter_heading": "Suscríbete a nuestro newsletter",
- "newsletter_description": "Recibe las últimas noticias, ofertas y actualizaciones",
- "copyright_text": "© 2024 Tu tienda. Todos los derechos reservados.",
-
- "quick_links": [
- { "title": "Inicio", "url": "/" },
- { "title": "Productos", "url": "/productos" },
- { "title": "Colecciones", "url": "/colecciones" },
- { "title": "Sobre nosotros", "url": "/sobre-nosotros" },
- { "title": "Contacto", "url": "/contacto" }
- ],
-
- "info_links": [
- { "title": "Política de privacidad", "url": "/politicas/privacidad" },
- { "title": "Términos y condiciones", "url": "/politicas/terminos" },
- { "title": "Política de envíos", "url": "/politicas/envios" },
- { "title": "Devoluciones", "url": "/politicas/devoluciones" },
- { "title": "Preguntas frecuentes", "url": "/faq" }
- ],
-
- "social_links": [
- { "platform": "facebook", "url": "", "active": false },
- { "platform": "instagram", "url": "", "active": false },
- { "platform": "twitter", "url": "", "active": false },
- { "platform": "youtube", "url": "", "active": false },
- { "platform": "tiktok", "url": "", "active": false }
- ],
-
- "payment_icons": ["visa", "mastercard", "paypal", "amex"],
-
- "layout": "four-columns",
- "show_divider": true
- }
+ "background": "#000000",
+ "text_color": "#F4F4F4"
+ },
+ "blocks": [
+ {
+ "id": "block-links-0",
+ "type": "links",
+ "settings": {
+ "text": "Inicio",
+ "link": "/"
+ }
+ },
+ {
+ "id": "block-links-1",
+ "type": "links",
+ "settings": {
+ "text": "Productos",
+ "link": "/productos"
+ }
+ },
+ {
+ "id": "block-links-2",
+ "type": "links",
+ "settings": {
+ "text": "Colecciones",
+ "link": "/colecciones"
+ }
+ },
+ {
+ "id": "block-links-3",
+ "type": "links",
+ "settings": {
+ "text": "Sobre nosotros",
+ "link": "/sobre-nosotros"
+ }
+ },
+ {
+ "id": "block-links-4",
+ "type": "links",
+ "settings": {
+ "text": "Contacto",
+ "link": "/contacto"
+ }
+ },
+ {
+ "id": "block-links-5",
+ "type": "links",
+ "settings": {
+ "text": "Política de privacidad",
+ "link": "/politicas/privacidad"
+ }
+ },
+ {
+ "id": "block-links-6",
+ "type": "links",
+ "settings": {
+ "text": "Términos y condiciones",
+ "link": "/politicas/terminos"
+ }
+ },
+ {
+ "id": "block-links-7",
+ "type": "links",
+ "settings": {
+ "text": "Política de envíos",
+ "link": "/politicas/envios"
+ }
+ },
+ {
+ "id": "block-links-8",
+ "type": "links",
+ "settings": {
+ "text": "Devoluciones",
+ "link": "/politicas/devoluciones"
+ }
+ },
+ {
+ "id": "block-links-9",
+ "type": "links",
+ "settings": {
+ "text": "Preguntas frecuentes",
+ "link": "/faq"
+ }
+ }
+ ]
}
}
diff --git a/app/[store]/products/[product]/page.tsx b/app/[store]/products/[product]/page.tsx
deleted file mode 100644
index 1079caf3..00000000
--- a/app/[store]/products/[product]/page.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { Metadata } from 'next'
-import { notFound } from 'next/navigation'
-import { storeRenderer } from '@/lib/store-renderer'
-
-// Forzar renderizado dinámico para acceder a variables de entorno en runtime
-export const dynamic = 'force-dynamic'
-
-interface ProductPageProps {
- params: Promise<{
- store: string
- product: string
- }>
- searchParams: Promise<{
- [key: string]: string | string[] | undefined
- }>
-}
-
-/**
- * Página de producto individual con SSR
- * Ruta: /[store]/products/[product]
- */
-export default async function ProductPage({ params }: ProductPageProps) {
- const resolvedParams = await params
- const { store, product } = resolvedParams
-
- try {
- // Construir dominio y path
- const domain = `${store}.fasttify.com`
- const path = `/products/${product}`
-
- // Renderizar página de producto
- const result = await storeRenderer.renderPage(domain, path)
-
- return
- } catch (error: any) {
- console.error(`Error rendering product page ${store}/products/${product}:`, error)
-
- // 404 para productos no encontrados
- if (error.type === 'DATA_ERROR' || error.statusCode === 404) {
- notFound()
- }
-
- // Re-lanzar otros errores para error boundary
- throw error
- }
-}
-
-/**
- * Genera metadata SEO específica para productos
- */
-export async function generateMetadata({ params }: ProductPageProps): Promise {
- const resolvedParams = await params
- const { store, product } = resolvedParams
-
- try {
- const domain = `${store}.fasttify.com`
- const path = `/products/${product}`
- const result = await storeRenderer.renderPage(domain, path)
-
- const { metadata } = result
-
- // Metadata optimizada para productos (e-commerce)
- return {
- title: metadata.title,
- description: metadata.description,
- alternates: {
- canonical: metadata.canonical,
- },
- openGraph: metadata.openGraph
- ? {
- title: metadata.openGraph.title,
- description: metadata.openGraph.description,
- url: metadata.openGraph.url,
- type: 'website', // Para productos específicos
- images: metadata.openGraph.image
- ? [
- {
- url: metadata.openGraph.image,
- alt: metadata.openGraph.title,
- },
- ]
- : undefined,
- siteName: metadata.openGraph.site_name,
- }
- : undefined,
- twitter: metadata.openGraph
- ? {
- card: 'summary_large_image',
- title: metadata.openGraph.title,
- description: metadata.openGraph.description,
- images: metadata.openGraph.image ? [metadata.openGraph.image] : undefined,
- }
- : undefined,
- // Schema.org para productos
- other: metadata.schema
- ? {
- 'application-ld+json': JSON.stringify(metadata.schema),
- }
- : undefined,
- // Keywords específicos para productos
- keywords: [product.replace(/-/g, ' '), store, 'comprar online', 'tienda', 'producto'].join(
- ', '
- ),
- }
- } catch (error) {
- console.error(`Error generating product metadata for ${store}/products/${product}:`, error)
-
- // Metadata de fallback
- const productName = product.replace(/-/g, ' ')
- return {
- title: `${productName} | ${store}`,
- description: `Compra ${productName} en ${store}. ¡Envío rápido y seguro!`,
- keywords: `${productName}, ${store}, comprar online`,
- }
- }
-}
-
-/**
- * COMENTADO: Estas configuraciones estáticas conflictan con force-dynamic
- * No se pueden usar juntas en Next.js 15
- */
-
-// export const revalidate = 900 // 15 minutos
-
-// export async function generateStaticParams() {
-// return []
-// }
diff --git a/app/api/stores/[storeId]/assets/[...path]/route.ts b/app/api/stores/[storeId]/assets/[...path]/route.ts
new file mode 100644
index 00000000..8520b351
--- /dev/null
+++ b/app/api/stores/[storeId]/assets/[...path]/route.ts
@@ -0,0 +1,154 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
+
+// Configuración de S3
+const s3Client = new S3Client({
+ region: process.env.REGION_BUCKET || 'us-east-2',
+})
+
+// Configuración de entorno
+const bucketName = process.env.BUCKET_NAME || ''
+const cloudFrontDomain = process.env.CLOUDFRONT_DOMAIN_NAME || ''
+const appEnv = process.env.APP_ENV || 'development'
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: Promise<{ storeId: string; path: string[] }> }
+) {
+ try {
+ const { storeId, path } = await params
+ const assetPath = path.join('/')
+
+ let buffer: Buffer
+ let etag: string | undefined
+
+ // En producción usar CloudFront, en desarrollo usar S3 directo
+ if (appEnv === 'production' && cloudFrontDomain) {
+ const result = await loadAssetFromCloudFront(storeId, assetPath)
+ buffer = result.buffer
+ etag = result.etag
+ } else {
+ const result = await loadAssetFromS3(storeId, assetPath)
+ buffer = result.buffer
+ etag = result.etag
+ }
+
+ // Determinar content type
+ const contentType = getContentTypeFromFilename(assetPath)
+
+ console.log(`[AssetsAPI] Serving asset: ${assetPath} (${contentType}) - ${buffer.length} bytes`)
+
+ // Retornar el archivo con headers apropiados
+ return new NextResponse(new Uint8Array(buffer), {
+ status: 200,
+ headers: {
+ 'Content-Type': contentType,
+ 'Content-Length': buffer.length.toString(),
+ 'Cache-Control': 'public, max-age=31536000', // Cache por 1 año
+ ETag: etag || '',
+ },
+ })
+ } catch (error) {
+ console.error('[AssetsAPI] Error loading asset:', error)
+
+ if (error instanceof Error && error.name === 'NoSuchKey') {
+ return NextResponse.json({ error: 'Asset not found' }, { status: 404 })
+ }
+
+ return NextResponse.json(
+ {
+ error: 'Internal server error',
+ details: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 }
+ )
+ }
+}
+
+// Helper function para cargar asset desde CloudFront (producción)
+async function loadAssetFromCloudFront(
+ storeId: string,
+ assetPath: string
+): Promise<{ buffer: Buffer; etag?: string }> {
+ const assetUrl = `https://${cloudFrontDomain}/templates/${storeId}/assets/${assetPath}`
+
+ const response = await fetch(assetUrl)
+
+ if (!response.ok) {
+ throw new Error(`Asset not found: ${assetPath} (CloudFront returned ${response.status})`)
+ }
+
+ const arrayBuffer = await response.arrayBuffer()
+ const buffer = Buffer.from(arrayBuffer)
+ const etag = response.headers.get('etag') || undefined
+
+ return { buffer, etag }
+}
+
+// Helper function para cargar asset desde S3 (desarrollo)
+async function loadAssetFromS3(
+ storeId: string,
+ assetPath: string
+): Promise<{ buffer: Buffer; etag?: string }> {
+ if (!bucketName) {
+ throw new Error('S3 bucket not configured')
+ }
+
+ // Construir la key de S3
+ const s3Key = `templates/${storeId}/assets/${assetPath}`
+
+ // Obtener el archivo desde S3
+ const command = new GetObjectCommand({
+ Bucket: bucketName,
+ Key: s3Key,
+ })
+
+ const response = await s3Client.send(command)
+
+ if (!response.Body) {
+ throw new Error(`Asset not found: ${assetPath}`)
+ }
+
+ // Convertir stream a buffer
+ const buffer = await streamToBuffer(response.Body)
+ const etag = response.ETag
+
+ return { buffer, etag }
+}
+
+// Helper function para convertir stream a buffer
+async function streamToBuffer(stream: any): Promise {
+ const chunks: Uint8Array[] = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ return Buffer.concat(chunks)
+}
+
+// Helper function para determinar content type
+function getContentTypeFromFilename(filename: string): string {
+ const ext = filename.toLowerCase().split('.').pop()
+
+ const contentTypes: Record = {
+ // Imágenes
+ png: 'image/png',
+ jpg: 'image/jpeg',
+ jpeg: 'image/jpeg',
+ gif: 'image/gif',
+ svg: 'image/svg+xml',
+ webp: 'image/webp',
+ ico: 'image/x-icon',
+ // CSS y JS
+ css: 'text/css',
+ js: 'application/javascript',
+ // Fonts
+ woff: 'font/woff',
+ woff2: 'font/woff2',
+ ttf: 'font/ttf',
+ eot: 'application/vnd.ms-fontobject',
+ }
+
+ return contentTypes[ext || ''] || 'application/octet-stream'
+}
diff --git a/app/api/stores/render/route.ts b/app/api/stores/render/route.ts
index 420ab1c8..5dfa0bae 100644
--- a/app/api/stores/render/route.ts
+++ b/app/api/stores/render/route.ts
@@ -108,34 +108,10 @@ function generateFullHTML(body: string, metadata: any): string {
-
-
-
+
- ${body}
-
+ ${body}