@ecomiq/storefront 2.0.7 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "Micro framework SSR de Ecomiq para storefronts. Páginas, datos y deploy sin configurar nada.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,6 +41,7 @@
41
41
  "./internal/checkout": "./src/pages/checkout.tsx",
42
42
  "./internal/checkout-session": "./src/pages/checkout.$checkoutId.tsx",
43
43
  "./internal/thank-you": "./src/pages/thank-you.tsx",
44
+ "./internal/cms-page": "./src/pages/pages.$pageSlug.tsx",
44
45
  "./package.json": "./package.json"
45
46
  },
46
47
  "files": [
@@ -75,7 +76,7 @@
75
76
  "vite": "^8.1.0",
76
77
  "wrangler": "^4.100.0",
77
78
  "zod": "^3.25.0",
78
- "@ecomiq/core": "^0.1.1"
79
+ "@ecomiq/core": "^0.1.2"
79
80
  },
80
81
  "peerDependencies": {
81
82
  "react": "^19.0.0",
package/src/codegen.mjs CHANGED
@@ -57,6 +57,11 @@ const FRAMEWORK_LOCKED_PAGES = [
57
57
  routeId: '/thank-you',
58
58
  importFrom: '@ecomiq/storefront/internal/thank-you',
59
59
  },
60
+ {
61
+ rel: 'pages.$pageSlug.tsx',
62
+ routeId: '/pages/$pageSlug',
63
+ importFrom: '@ecomiq/storefront/internal/cms-page',
64
+ },
60
65
  ]
61
66
 
62
67
  export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
@@ -487,7 +492,7 @@ function assertNoFrameworkPageOverrides(pageEntries) {
487
492
  if (!reserved.has(routeId)) continue
488
493
  throw new Error(
489
494
  `La ruta ${routeId} es reservada por Ecomiq (página cerrada). ` +
490
- `Elimina pages/${entry.rel}; las páginas de checkout y thank-you viven en @ecomiq/storefront.`,
495
+ `Elimina pages/${entry.rel}; vive en @ecomiq/storefront (checkout, thank-you, CMS).`,
491
496
  )
492
497
  }
493
498
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Página CMS cerrada (`/pages/$pageSlug`).
3
+ * Resuelve contenido publicado vía GET /v1/pages/{slug}.
4
+ */
5
+ import type { CmsPageDetailsDto } from '@ecomiq/core'
6
+ import {
7
+ definePage,
8
+ sanitizeHtml,
9
+ serverApi,
10
+ stripHtml,
11
+ z,
12
+ } from '../index'
13
+ import { StorefrontError } from '../ui'
14
+
15
+ const ContentPageSettingsSchema = z.object({
16
+ title: z.string().optional(),
17
+ html: z.string().optional(),
18
+ seo: z.object({
19
+ title: z.string().optional(),
20
+ description: z.string().optional(),
21
+ }).optional(),
22
+ }).passthrough()
23
+
24
+ type ContentPageData = {
25
+ page: CmsPageDetailsDto
26
+ content: z.infer<typeof ContentPageSettingsSchema>
27
+ }
28
+
29
+ export default definePage<ContentPageData, { pageSlug: string }>({
30
+ loader: async ({ params }) => {
31
+ const page = await serverApi.getPageBySlug(params.pageSlug)
32
+ if (page.status !== 'published') throw new Error('Página no publicada')
33
+
34
+ return {
35
+ page,
36
+ content: ContentPageSettingsSchema.parse(page.settings ?? {}),
37
+ }
38
+ },
39
+
40
+ meta: ({ data }) => ({
41
+ title: data.content.seo?.title ?? data.content.title ?? data.page.name,
42
+ description: data.content.seo?.description ?? (
43
+ data.content.html
44
+ ? stripHtml(data.content.html).slice(0, 155)
45
+ : undefined
46
+ ),
47
+ }),
48
+
49
+ error: () => (
50
+ <StorefrontError
51
+ title="Página no disponible"
52
+ description="El contenido no existe o todavía no está publicado."
53
+ />
54
+ ),
55
+
56
+ component: ({ data }) => (
57
+ <article className="mx-auto w-full max-w-4xl px-4 py-10 sm:py-14">
58
+ <header className="mb-8 border-b pb-8">
59
+ <h1 className="text-balance text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
60
+ {data.content.title ?? data.page.name}
61
+ </h1>
62
+ </header>
63
+
64
+ {data.content.html ? (
65
+ <div
66
+ className="prose prose-neutral max-w-none text-foreground prose-headings:font-semibold prose-a:text-primary"
67
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(data.content.html) }}
68
+ />
69
+ ) : (
70
+ <p className="text-muted-foreground">Contenido no disponible.</p>
71
+ )}
72
+ </article>
73
+ ),
74
+ })
package/src/server.ts CHANGED
@@ -86,6 +86,10 @@ const getCollectionBySlugFn = createServerFn({ method: 'GET' })
86
86
  .validator(z.object({ slug: z.string().min(1) }))
87
87
  .handler(({ data }) => getStorefrontClient().getCollectionBySlug(data.slug))
88
88
 
89
+ const getPageBySlugFn = createServerFn({ method: 'GET' })
90
+ .validator(z.object({ slug: z.string().min(1) }))
91
+ .handler(({ data }) => getStorefrontClient().getPageBySlug(data.slug))
92
+
89
93
  const getProductsFn = createServerFn({ method: 'GET' })
90
94
  .validator(ProductsParamsSchema)
91
95
  .handler(({ data }) => getStorefrontClient().getProducts(data))
@@ -219,6 +223,7 @@ export const serverApi = {
219
223
  getStoreId: () => getStoreIdFn(),
220
224
  getCollections: () => getCollectionsFn(),
221
225
  getCollectionBySlug: (slug: string) => getCollectionBySlugFn({ data: { slug } }),
226
+ getPageBySlug: (slug: string) => getPageBySlugFn({ data: { slug } }),
222
227
  getProducts: (
223
228
  params: ProductsParamsInput = {},
224
229
  options: { signal?: AbortSignal } = {},