@marvalt/digivalt-core 0.2.8 → 0.2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.9] - 2026-04-03
9
+
10
+ ### Added
11
+ - **Template + init:** **`functions/sitemap.xml.ts`** — Cloudflare Pages handler for **`/sitemap.xml`** using **`VITE_SEO_SITE_URL`** / **`SITE_URL`** and **`public/sitemap-paths.json`** (no WordPress). Default **`template/public/sitemap-paths.json`**. Listed in **`TEMPLATE_MANAGED_FILES`** for `--force-template` refresh of the handler only.
12
+
13
+ ### Changed
14
+ - **Docs:** `DIGIVALT_SETUP.md`, **`docs/SEO-PERFORMANCE.md`** — sitemap section.
15
+
8
16
  ## [0.2.8] - 2026-04-03
9
17
 
10
18
  ### Changed
package/bin/init.cjs CHANGED
@@ -24,6 +24,7 @@ const TEMPLATE_MANAGED_FILES = new Set([
24
24
  '.dev.vars.example',
25
25
  'DIGIVALT_SETUP.md',
26
26
  'functions/api/fetch-with-access.ts',
27
+ 'functions/sitemap.xml.ts',
27
28
  'functions/api/gravity-forms-submit.ts',
28
29
  'functions/api/mautic-submit.ts',
29
30
  'functions/api/webhook.js',
@@ -41,6 +41,10 @@ Read via `getPerformanceConfig()` from `@marvalt/digivalt-core/config`. Set to `
41
41
  - `VITE_PERF_SERVICE_WORKER` (default off)
42
42
  - `VITE_PERF_CACHING` (default on)
43
43
 
44
+ ## Sitemap (`/sitemap.xml`)
45
+
46
+ DigiValt apps ship a **Cloudflare Pages Function** at **`functions/sitemap.xml.ts`** that responds at **`/sitemap.xml`**. It reads **`VITE_SEO_SITE_URL`** (or **`SITE_URL`**) for canonical `<loc>` values and loads path slugs from **`public/sitemap-paths.json`** (shape: `{ "paths": ["/", "/pricing"] }`). Update that JSON when you add marketing routes; avoid a static **`public/sitemap.xml`** file, which would override the Function.
47
+
44
48
  ## Imports
45
49
 
46
50
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marvalt/digivalt-core",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Core glue logic and shared context for DigiVAlt frontend applications",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "main": "dist/index.cjs",
@@ -84,6 +84,7 @@ Init will:
84
84
  1. Install **`react-helmet-async`** and wrap your app with **`HelmetProvider`** at the root (next to the router).
85
85
  2. Set at least **`VITE_SEO_SITE_URL`** (no trailing slash), **`VITE_SEO_SITE_NAME`**, **`VITE_SEO_DEFAULT_TITLE`**, **`VITE_SEO_DEFAULT_DESCRIPTION`**, and **`VITE_SEO_DEFAULT_OG_IMAGE`** in Cloudflare Pages / `.env.local`.
86
86
  3. Import **`DigiValtSeoHead`** and **`JsonLd`** from **`@marvalt/digivalt-core/seo`** in your layouts or page components; pass **`pathname`** per route for correct canonical URLs.
87
- 4. Read the full guide shipped with the package: **`node_modules/@marvalt/digivalt-core/docs/SEO-PERFORMANCE.md`** (or in the repo: `digivalt-npm-packages/digivalt-core/docs/SEO-PERFORMANCE.md`).
87
+ 4. **Sitemap:** After init, **`/sitemap.xml`** is served by **`functions/sitemap.xml.ts`** (Cloudflare Pages). Canonical URLs use **`VITE_SEO_SITE_URL`** (or **`SITE_URL`**); edit **`public/sitemap-paths.json`** (`{ "paths": ["/", "/about"] }`) when you add public routes. Do not add a static **`public/sitemap.xml`** (it would shadow the Function).
88
+ 5. Read the full guide shipped with the package: **`node_modules/@marvalt/digivalt-core/docs/SEO-PERFORMANCE.md`** (or in the repo: `digivalt-npm-packages/digivalt-core/docs/SEO-PERFORMANCE.md`).
88
89
 
89
90
  You are fully onboarded and ready to develop offline!
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Serves /sitemap.xml from canonical site URL + public/sitemap-paths.json (no WordPress).
3
+ * Set VITE_SEO_SITE_URL (or SITE_URL) on Cloudflare Pages for production canonical hosts.
4
+ */
5
+
6
+ function normalizeBase(raw: string | undefined): string | null {
7
+ if (!raw || typeof raw !== 'string') return null;
8
+ const t = raw.trim().replace(/\/+$/, '');
9
+ return t || null;
10
+ }
11
+
12
+ function escapeXml(s: string): string {
13
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
14
+ }
15
+
16
+ async function loadPaths(request: Request): Promise<string[]> {
17
+ try {
18
+ const jsonUrl = new URL('/sitemap-paths.json', request.url);
19
+ const res = await fetch(jsonUrl.toString());
20
+ if (!res.ok) return ['/'];
21
+ const data: unknown = await res.json();
22
+ const list: string[] = [];
23
+ if (Array.isArray(data)) {
24
+ for (const p of data) {
25
+ if (typeof p === 'string' && p.startsWith('/')) list.push(p);
26
+ }
27
+ } else if (data && typeof data === 'object' && Array.isArray((data as { paths?: unknown }).paths)) {
28
+ for (const p of (data as { paths: unknown[] }).paths) {
29
+ if (typeof p === 'string' && p.startsWith('/')) list.push(p);
30
+ }
31
+ }
32
+ return list.length ? list : ['/'];
33
+ } catch {
34
+ return ['/'];
35
+ }
36
+ }
37
+
38
+ function buildSitemap(base: string, paths: string[]): string {
39
+ const lines = paths.map((p) => {
40
+ const loc = p === '/' ? base : `${base}${p.startsWith('/') ? p : `/${p}`}`;
41
+ return ` <url><loc>${escapeXml(loc)}</loc></url>`;
42
+ });
43
+ return `<?xml version="1.0" encoding="UTF-8"?>
44
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
45
+ ${lines.join('\n')}
46
+ </urlset>`;
47
+ }
48
+
49
+ export async function onRequestGet(context: { request: Request; env: Record<string, string | undefined> }) {
50
+ const { request, env } = context;
51
+ const fromEnv = normalizeBase(env.VITE_SEO_SITE_URL || env.SITE_URL);
52
+ const origin = new URL(request.url).origin.replace(/\/+$/, '');
53
+ const base = fromEnv || origin;
54
+
55
+ const rawPaths = await loadPaths(request);
56
+ const paths = [...new Set(rawPaths.length ? rawPaths : ['/'])];
57
+
58
+ const xml = buildSitemap(base, paths);
59
+ return new Response(xml, {
60
+ headers: {
61
+ 'Content-Type': 'application/xml; charset=utf-8',
62
+ 'Cache-Control': 'public, max-age=300',
63
+ },
64
+ });
65
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "paths": ["/"]
3
+ }