@decocms/apps 1.11.1 → 1.12.0

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": "@decocms/apps",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "type": "module",
5
5
  "description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
6
6
  "exports": {
@@ -1,9 +1,14 @@
1
1
  /**
2
- * VTEX Sitemap utility.
2
+ * VTEX Sitemap utilities.
3
3
  *
4
- * Fetches product and category URLs from VTEX's sitemap API
5
- * and converts them to SitemapEntry format for composition
6
- * with the CMS sitemap generator.
4
+ * Two flavors:
5
+ * - `getVtexSitemapEntries()` flatten VTEX sub-sitemaps into a single
6
+ * `SitemapEntry[]` list, for composition with the CMS sitemap generator.
7
+ * - `createVtexSitemapProxy()` — proxy `/sitemap.xml` and `/sitemap/*`
8
+ * straight from VTEX's commerce-stable origin, preserving the sitemap-index
9
+ * shape (so crawlers stay within Google's per-file size limit). This is the
10
+ * right choice when the storefront has no native sitemap renderer and just
11
+ * needs to expose VTEX's existing crawl tree to the public hostname.
7
12
  */
8
13
 
9
14
  import { getVtexConfig, vtexFetchResponse, vtexHost } from "../client";
@@ -131,3 +136,147 @@ function rewriteUrl(url: string, vtexSitemapHost: string, origin: string): strin
131
136
  return url.replace(`https://${vtexSitemapHost}`, origin);
132
137
  }
133
138
  }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // VTEX sitemap proxy factory
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /**
145
+ * Returns true if `pathname` is one of the proxied sitemap paths
146
+ * (`/sitemap.xml` or any `/sitemap/*` sub-sitemap).
147
+ */
148
+ export function isVtexSitemapPath(pathname: string): boolean {
149
+ return pathname === "/sitemap.xml" || pathname.startsWith("/sitemap/");
150
+ }
151
+
152
+ export interface VtexSitemapProxyConfig {
153
+ /**
154
+ * Extra `<sitemap>` entries to inject into the root sitemap index
155
+ * (`/sitemap.xml` only — sub-sitemaps are passed through untouched).
156
+ *
157
+ * Useful for site-managed sitemaps such as a static search-result
158
+ * index (`sitemap-busca.xml`) that VTEX doesn't generate.
159
+ *
160
+ * Each value is normalized to an absolute URL on the storefront
161
+ * origin: leading-slash paths become `${origin}${path}`, and bare
162
+ * names become `${origin}/${name}`. Absolute URLs are used as-is.
163
+ *
164
+ * @example ["/sitemap-busca.xml"]
165
+ */
166
+ extraSitemaps?: string[];
167
+
168
+ /**
169
+ * VTEX environment for the upstream sitemap fetch.
170
+ * @default "vtexcommercestable"
171
+ */
172
+ environment?: "vtexcommercestable" | "vtexcommercebeta";
173
+
174
+ /**
175
+ * `Cache-Control` header to set on proxied responses. The default
176
+ * favors edge caching (Cloudflare honors `s-maxage`) with a long
177
+ * stale-while-revalidate window so a slow VTEX origin never blocks
178
+ * crawlers.
179
+ *
180
+ * @default "public, s-maxage=3600, stale-while-revalidate=86400"
181
+ */
182
+ cacheControl?: string;
183
+
184
+ /**
185
+ * Optional fetch override — primarily for tests. Defaults to the
186
+ * platform `fetch`.
187
+ */
188
+ fetchImpl?: typeof fetch;
189
+ }
190
+
191
+ const DEFAULT_SITEMAP_CACHE_CONTROL = "public, s-maxage=3600, stale-while-revalidate=86400";
192
+
193
+ function normalizeExtraSitemap(entry: string, origin: string): string {
194
+ if (entry.startsWith("http://") || entry.startsWith("https://")) return entry;
195
+ const path = entry.startsWith("/") ? entry : `/${entry}`;
196
+ return `${origin}${path}`;
197
+ }
198
+
199
+ /**
200
+ * Creates a sitemap proxy handler that mirrors VTEX's `/sitemap.xml`
201
+ * (and sub-sitemaps) onto the storefront origin.
202
+ *
203
+ * Returns a function compatible with `createDecoWorkerEntry`'s
204
+ * `proxyHandler`: it returns `null` for non-sitemap paths, so it
205
+ * composes naturally with other proxy handlers
206
+ * (`createVtexCheckoutProxy`, custom logic, etc.).
207
+ *
208
+ * The VTEX account is read from the `configureVtex(...)` call done at
209
+ * worker startup — no per-call account configuration is needed.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * import { createVtexSitemapProxy } from "@decocms/apps/vtex/utils/sitemap";
214
+ * import {
215
+ * createVtexCheckoutProxy,
216
+ * shouldProxyToVtex,
217
+ * } from "@decocms/apps/vtex/utils/proxy";
218
+ *
219
+ * const proxySitemap = createVtexSitemapProxy({
220
+ * extraSitemaps: ["/sitemap-busca.xml"], // optional, site-managed
221
+ * });
222
+ * const proxyCheckout = createVtexCheckoutProxy({ ... });
223
+ *
224
+ * createDecoWorkerEntry(serverEntry, {
225
+ * proxyHandler: async (request, url) => {
226
+ * const sitemap = await proxySitemap(request, url);
227
+ * if (sitemap) return sitemap;
228
+ *
229
+ * if (!shouldProxyToVtex(url.pathname)) return null;
230
+ * return proxyCheckout(request, url);
231
+ * },
232
+ * });
233
+ * ```
234
+ */
235
+ export function createVtexSitemapProxy(
236
+ config: VtexSitemapProxyConfig = {},
237
+ ): (request: Request, url: URL) => Promise<Response | null> {
238
+ const environment = config.environment ?? "vtexcommercestable";
239
+ const cacheControl = config.cacheControl ?? DEFAULT_SITEMAP_CACHE_CONTROL;
240
+ const extraSitemaps = config.extraSitemaps ?? [];
241
+ const fetchImpl = config.fetchImpl ?? fetch;
242
+
243
+ return async (_request: Request, url: URL): Promise<Response | null> => {
244
+ if (!isVtexSitemapPath(url.pathname)) return null;
245
+
246
+ // vtexHost() reads the configured account from configureVtex().
247
+ const vtexSitemapHost = vtexHost(environment);
248
+ const target = `https://${vtexSitemapHost}${url.pathname}`;
249
+
250
+ try {
251
+ const resp = await fetchImpl(target);
252
+ if (!resp.ok) {
253
+ console.error(`[vtex-sitemap] VTEX returned ${resp.status} for ${url.pathname}`);
254
+ return new Response("Sitemap temporarily unavailable", { status: 502 });
255
+ }
256
+
257
+ let xml = await resp.text();
258
+ xml = xml.replaceAll(`https://${vtexSitemapHost}`, url.origin);
259
+
260
+ if (url.pathname === "/sitemap.xml" && extraSitemaps.length > 0) {
261
+ const extraEntries = extraSitemaps
262
+ .map(
263
+ (s) =>
264
+ ` <sitemap>\n <loc>${normalizeExtraSitemap(s, url.origin)}</loc>\n </sitemap>`,
265
+ )
266
+ .join("\n");
267
+ xml = xml.replace("</sitemapindex>", `${extraEntries}\n</sitemapindex>`);
268
+ }
269
+
270
+ return new Response(xml, {
271
+ status: 200,
272
+ headers: {
273
+ "Content-Type": "application/xml; charset=utf-8",
274
+ "Cache-Control": cacheControl,
275
+ },
276
+ });
277
+ } catch (err) {
278
+ console.error("[vtex-sitemap] Failed to proxy VTEX sitemap:", err);
279
+ return new Response("Sitemap temporarily unavailable", { status: 502 });
280
+ }
281
+ };
282
+ }