@marvalt/digivalt-core 0.2.30 → 0.2.33
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 +35 -0
- package/GLOBAL-SHORTCODES.md +7 -7
- package/README.md +10 -8
- package/bin/env-example-fragments.cjs +6 -0
- package/bin/init.cjs +44 -5
- package/dist/generators/index.d.ts +3 -0
- package/dist/generators/prerender-seo.d.ts +6 -0
- package/dist/generators/seo-manifest.d.ts +3 -0
- package/dist/generators/seo-urls.d.ts +29 -0
- package/dist/generators/sitemap-paths.d.ts +5 -9
- package/dist/generators.cjs +1005 -155
- package/dist/generators.cjs.map +1 -1
- package/dist/generators.d.ts +86 -6
- package/dist/generators.esm.js +992 -156
- package/dist/generators.esm.js.map +1 -1
- package/dist/index.cjs +1019 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +103 -9
- package/dist/index.esm.js +1006 -157
- package/dist/index.esm.js.map +1 -1
- package/dist/registry/index.d.ts +20 -0
- package/dist/registry.cjs +1528 -0
- package/dist/registry.cjs.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.esm.js +1523 -0
- package/dist/registry.esm.js.map +1 -0
- package/dist/seo/DigiValtRouteSeo.d.ts +12 -0
- package/dist/seo/DigiValtSeoHead.d.ts +2 -0
- package/dist/seo/buildPageJsonLd.d.ts +11 -0
- package/dist/seo/html.d.ts +8 -0
- package/dist/seo/index.d.ts +7 -0
- package/dist/seo/resolvePageSeo.d.ts +17 -0
- package/dist/seo/seoTypes.d.ts +57 -0
- package/dist/seo.cjs +307 -86
- package/dist/seo.cjs.map +1 -1
- package/dist/seo.d.ts +105 -2
- package/dist/seo.esm.js +300 -87
- package/dist/seo.esm.js.map +1 -1
- package/dist/static/wpStatic.d.ts +16 -0
- package/dist/static.cjs +10 -0
- package/dist/static.cjs.map +1 -1
- package/dist/static.d.ts +38 -2
- package/dist/static.esm.js +10 -1
- package/dist/static.esm.js.map +1 -1
- package/dist/utils/notifications.d.ts +3 -0
- package/docs/AI-LOCAL-WORDPRESS-BOOTSTRAP.md +7 -3
- package/docs/AI-MASTER-PROMPT.md +37 -0
- package/docs/ENVIRONMENT.md +3 -3
- package/docs/README.md +6 -4
- package/docs/SEO-AIO.md +59 -0
- package/docs/SEO-PERFORMANCE.md +44 -11
- package/docs/SHORTCODES.md +4 -5
- package/package.json +17 -6
- package/template/DIGIVALT_SETUP.md +14 -8
- package/template/functions/robots.txt.ts +27 -0
- package/template/functions/sitemap.xml.ts +143 -65
- package/template/scripts/generate.ts +3 -10
- package/template/scripts/prerender-seo.ts +10 -0
- package/docs/LOVABLE-MASTER-PROMPT.md +0 -140
package/docs/SEO-PERFORMANCE.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# SEO and PageSpeed with `@marvalt/digivalt-core`
|
|
2
2
|
|
|
3
|
-
This guide is for **Vite + React** apps
|
|
3
|
+
This guide is for **Vite + React** apps that use DigiValt static generation. It complements WordPress content with **document metadata**, **structured data**, and **performance-minded image/font patterns**.
|
|
4
|
+
|
|
5
|
+
**How the system fits together** (WP vs env, prerender, editor workflow): **[SEO-AIO.md](./SEO-AIO.md)**. Team handbook: `digivalt-collaboration-docs/DIGIVALT_SEO_AIO.md`.
|
|
4
6
|
|
|
5
7
|
## Prerequisites
|
|
6
8
|
|
|
@@ -30,6 +32,10 @@ Use the **`VITE_SEO_*`** prefix so SEO-related keys are easy to copy into a chec
|
|
|
30
32
|
| `VITE_SEO_FACEBOOK_APP_ID` | Optional `fb:app_id`. |
|
|
31
33
|
| `VITE_SEO_GOOGLE_SITE_VERIFICATION` | Google Search Console meta content. |
|
|
32
34
|
| `VITE_SEO_BING_SITE_VERIFICATION` | Bing `msvalidate.01` content. |
|
|
35
|
+
| `VITE_SEO_ROUTE_PREFIXES` | Optional. Comma list `rest_base:/app-prefix` for CPT sitemap/prerender paths (e.g. `eventbrite_event:/events`). Pages are always `/{slug}` except the Reading Settings front page → `/`. |
|
|
36
|
+
| `VITE_SEO_FRONT_PAGE_SLUG` | Optional. Force this WP page slug to `/` when Reading Settings `front_page` does not match the app homepage. |
|
|
37
|
+
|
|
38
|
+
Per-URL title, description, canonical, robots, OG image, FAQ, and schema live in the **DigiValt SEO** WordPress plugin (`digivalt_seo` on the REST object), not in env.
|
|
33
39
|
|
|
34
40
|
### Performance toggles (optional)
|
|
35
41
|
|
|
@@ -41,24 +47,48 @@ Read via `getPerformanceConfig()` from `@marvalt/digivalt-core/config`. Set to `
|
|
|
41
47
|
- `VITE_PERF_SERVICE_WORKER` (default off)
|
|
42
48
|
- `VITE_PERF_CACHING` (default on)
|
|
43
49
|
|
|
44
|
-
## Sitemap (`/sitemap.xml`)
|
|
50
|
+
## Sitemap (`/sitemap.xml`) and robots
|
|
51
|
+
|
|
52
|
+
**Runtime:** Cloudflare Pages Functions:
|
|
53
|
+
|
|
54
|
+
- **`functions/sitemap.xml.ts`** → `/sitemap.xml` (canonical host from `VITE_SEO_SITE_URL` / `SITE_URL`)
|
|
55
|
+
- **`functions/robots.txt.ts`** → `/robots.txt` (`Allow: /` plus `Sitemap:`). Do not commit **`public/robots.txt`**; it would shadow the Function.
|
|
56
|
+
|
|
57
|
+
Do not add **`public/sitemap.xml`**.
|
|
58
|
+
|
|
59
|
+
**Build time:** `generateSitemapPaths` writes **`public/sitemap-paths.json`**:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{ "urls": [ { "path": "/", "lastmod": "2026-09-18T00:00:00Z", "image": "https://…" } ] }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A legacy `paths: string[]` array is still accepted by the Function. If there are more than 45k URLs, an index plus `sitemap-paths-N.json` shards are written; `/sitemap.xml` then emits a sitemap index.
|
|
66
|
+
|
|
67
|
+
Sources (frontend routes only — never raw CMS permalinks):
|
|
68
|
+
|
|
69
|
+
1. **`public/sitemap-static-paths.json`** — app-owned public routes (not utility `noindex` URLs).
|
|
70
|
+
2. Published **pages**: front page (Reading Settings `front_page` slug or id) → `/`; other pages → `/{slug}`. `digivalt_seo.robots_index === false` and non-`publish` statuses are omitted.
|
|
71
|
+
3. Other post types only when listed in **`VITE_SEO_ROUTE_PREFIXES`**.
|
|
72
|
+
4. PHPList issues from **`phplist-data.json`** under **`VITE_PHPLIST_NEWSLETTER_ROUTE_BASE`** (default `/newsletters`).
|
|
73
|
+
5. Header and footer menus: URL kept only if it already maps to a collected app path.
|
|
45
74
|
|
|
46
|
-
|
|
75
|
+
`lastmod` comes from `modified_gmt` / `modified` / newsletter `sent_at`. Do not emit fake `changefreq` or `priority`.
|
|
47
76
|
|
|
48
|
-
|
|
77
|
+
Fixture mode (`VITE_IS_LOVABLE=true`) still regenerates sitemap + **`seo-manifest.json`** from committed JSON.
|
|
49
78
|
|
|
50
|
-
|
|
51
|
-
2. **`navigation_menu`** items in **`wordpress-data.json`** (recursive `url` fields), filtered to **`http(s)`** and the CMS host from **`VITE_WORDPRESS_API_URL`** / **`WORDPRESS_API_URL`**.
|
|
52
|
-
3. **`link`** on entities in top-level arrays whose keys match **`config.post_types`** (fallback: other top-level arrays minus a fixed skip list). Taxonomy archives are omitted by default.
|
|
79
|
+
## Prerender (same HTML for humans and crawlers)
|
|
53
80
|
|
|
54
|
-
|
|
81
|
+
`generateSeoManifest` writes **`public/seo-manifest.json`**. After `vite build`, **`prerenderSeoHtml`** (template `scripts/prerender-seo.ts`) copies `dist/index.html` per indexable path, injects `<head>` and a crawlable snapshot inside `#root`. Client JS hydrates as usual. This is not React SSR.
|
|
55
82
|
|
|
56
83
|
## Imports
|
|
57
84
|
|
|
58
85
|
```ts
|
|
59
86
|
import {
|
|
87
|
+
DigiValtRouteSeo,
|
|
60
88
|
DigiValtSeoHead,
|
|
61
89
|
JsonLd,
|
|
90
|
+
resolvePageSeo,
|
|
91
|
+
buildPageJsonLd,
|
|
62
92
|
getSeoDefaultsFromEnv,
|
|
63
93
|
buildEventJsonLd,
|
|
64
94
|
createOrganizationSchema,
|
|
@@ -72,8 +102,9 @@ Prefer **`@marvalt/digivalt-core/seo`** so build scripts that only import `gener
|
|
|
72
102
|
|
|
73
103
|
## SPA behaviour
|
|
74
104
|
|
|
75
|
-
- **`
|
|
76
|
-
-
|
|
105
|
+
- **`DigiValtRouteSeo`** (preferred) sets Helmet + JSON-LD from a WordPress entity’s `digivalt_seo` field, falling back to visible title/excerpt then `VITE_SEO_*`.
|
|
106
|
+
- **`DigiValtSeoHead`** still updates `<head>` on the **client** for in-app navigation.
|
|
107
|
+
- **Crawlers:** prerendered `dist/{path}/index.html` snapshots carry title, meta, JSON-LD, and body HTML without executing JavaScript.
|
|
77
108
|
- **Analytics:** Fire page views on client route changes so funnels match SPA navigation (measurement, not a ranking factor).
|
|
78
109
|
|
|
79
110
|
## Components
|
|
@@ -146,7 +177,9 @@ After `npx digivalt-init`, add:
|
|
|
146
177
|
1. `npm install react-helmet-async`
|
|
147
178
|
2. `HelmetProvider` at the app root
|
|
148
179
|
3. SEO env vars (at least `VITE_SEO_SITE_URL`, `VITE_SEO_SITE_NAME`, `VITE_SEO_DEFAULT_OG_IMAGE` for production)
|
|
149
|
-
4. Per-route **`
|
|
180
|
+
4. Per-route **`DigiValtRouteSeo`** (or `DigiValtSeoHead` + `JsonLd`)
|
|
181
|
+
5. Install **DigiValt SEO** on WordPress for per-URL fields
|
|
182
|
+
6. Confirm build runs generate **and** `scripts/prerender-seo.ts`
|
|
150
183
|
|
|
151
184
|
## Upgrading from older `@marvalt/digivalt-core`
|
|
152
185
|
|
package/docs/SHORTCODES.md
CHANGED
|
@@ -4,7 +4,7 @@ After install: `node_modules/@marvalt/digivalt-core/docs/SHORTCODES.md`.
|
|
|
4
4
|
|
|
5
5
|
The same list lives at the package root as `GLOBAL-SHORTCODES.md`.
|
|
6
6
|
|
|
7
|
-
Content editors type shortcodes into WordPress. `@marvalt/wparser` finds `[name attr="value"]` in block HTML. `
|
|
7
|
+
Content editors type shortcodes into WordPress. `@marvalt/wparser` finds `[name attr="value"]` in block HTML. `createContentRegistry()` from `@marvalt/digivalt-core/registry` maps package-supported names to React components. `digivalt-init` does not copy a registry file.
|
|
8
8
|
|
|
9
9
|
**Do not register these as WordPress PHP shortcodes.** They must stay literal in REST content.
|
|
10
10
|
|
|
@@ -15,18 +15,17 @@ Content editors type shortcodes into WordPress. `@marvalt/wparser` finds `[name
|
|
|
15
15
|
| `[mautic_form id="1"]` | `@marvalt/madapter` `getMauticShortcodeHandlers()` | Optional `title`, `description`. See `node_modules/@marvalt/madapter/docs/SHORTCODES.md`. |
|
|
16
16
|
| `[phplist_subscribe list="1" attrs="firstname,lastname"]` | `@marvalt/plister` `getPhplistShortcodeHandlers()` | Visible fields only. Optional `page`, `title`, `intro`. See `node_modules/@marvalt/plister/docs/SHORTCODES.md`. |
|
|
17
17
|
| `[gf_embed id="1"]` / `[gform …]` | core | Gravity Forms inline. |
|
|
18
|
-
| `[popupbtn_gf …]` / `[popupbtn_mautic …]` | core |
|
|
19
|
-
| `[member_*]` / `[testimonial_*]` / `[event_*]` | `@marvalt/shcoder` | CPT cards, grids, slideshows. |
|
|
18
|
+
| `[popupbtn_gf …]` / `[popupbtn_mautic …]` | core | Package-local popup controls. |
|
|
20
19
|
|
|
21
20
|
Full attribute list: package root [GLOBAL-SHORTCODES.md](../GLOBAL-SHORTCODES.md).
|
|
22
21
|
|
|
23
22
|
## If shortcodes stay as raw text
|
|
24
23
|
|
|
25
|
-
1. The shortcode is not in `registry.shortcodes` —
|
|
24
|
+
1. The shortcode is not in `registry.shortcodes` — use `createContentRegistry()` or merge the required custom handler.
|
|
26
25
|
2. Someone registered a PHP shortcode in WordPress — undo that so the token remains in the API response.
|
|
27
26
|
3. Mautic/PHPList JSON was not generated, or `QueryClientProvider` is missing.
|
|
28
27
|
|
|
29
|
-
## Standalone
|
|
28
|
+
## Standalone custom registry
|
|
30
29
|
|
|
31
30
|
```tsx
|
|
32
31
|
import { getMauticShortcodeHandlers } from '@marvalt/madapter';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marvalt/digivalt-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
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",
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
"import": "./dist/seo.esm.js",
|
|
43
43
|
"require": "./dist/seo.cjs"
|
|
44
44
|
},
|
|
45
|
+
"./registry": {
|
|
46
|
+
"types": "./dist/registry.d.ts",
|
|
47
|
+
"import": "./dist/registry.esm.js",
|
|
48
|
+
"require": "./dist/registry.cjs"
|
|
49
|
+
},
|
|
45
50
|
"./integrations": {
|
|
46
51
|
"types": "./dist/integrations.d.ts",
|
|
47
52
|
"import": "./dist/integrations.esm.js",
|
|
@@ -101,24 +106,30 @@
|
|
|
101
106
|
"typescript": "^5.5.3"
|
|
102
107
|
},
|
|
103
108
|
"peerDependencies": {
|
|
109
|
+
"@tanstack/react-query": ">=5",
|
|
104
110
|
"react": ">=18",
|
|
105
111
|
"react-dom": ">=18",
|
|
106
112
|
"react-helmet-async": ">=2"
|
|
107
113
|
},
|
|
114
|
+
"peerDependenciesMeta": {
|
|
115
|
+
"@tanstack/react-query": {
|
|
116
|
+
"optional": true
|
|
117
|
+
}
|
|
118
|
+
},
|
|
108
119
|
"engines": {
|
|
109
120
|
"node": ">=18.0.0",
|
|
110
121
|
"npm": ">=8.0.0"
|
|
111
122
|
},
|
|
112
123
|
"dependencies": {
|
|
113
124
|
"@digivalt/tracker": "^0.1.1",
|
|
114
|
-
"@marvalt/dstyler": "^0.1.
|
|
125
|
+
"@marvalt/dstyler": "^0.1.53",
|
|
115
126
|
"@marvalt/madapter": "^2.3.11",
|
|
116
127
|
"@marvalt/plister": "^0.3.0",
|
|
117
|
-
"@marvalt/sadapter": "^0.1.
|
|
118
|
-
"@marvalt/shcoder": "^0.1.
|
|
119
|
-
"@marvalt/tadapter": "^1.0.
|
|
128
|
+
"@marvalt/sadapter": "^0.1.6",
|
|
129
|
+
"@marvalt/shcoder": "^0.1.37",
|
|
130
|
+
"@marvalt/tadapter": "^1.0.14",
|
|
120
131
|
"@marvalt/wadapter": "^2.3.61",
|
|
121
|
-
"@marvalt/wparser": "^0.1.
|
|
132
|
+
"@marvalt/wparser": "^0.1.82",
|
|
122
133
|
"@radix-ui/react-toast": "^1.2.15",
|
|
123
134
|
"class-variance-authority": "^0.7.1",
|
|
124
135
|
"clsx": "^2.1.1",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Welcome to your new **DigiVAlt** architecture project!
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Your Vite interface is decoupled from the data pipeline. `@marvalt/digivalt-core` supplies static generation and runtime helpers for your Cloudflare Pages app.
|
|
6
6
|
|
|
7
|
-
Follow these instructions
|
|
7
|
+
Follow these instructions to hydrate a new Vite project with real data.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ npm install -D vite-node
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## 🛠️ 2. Wire the Build Scripts
|
|
20
|
-
`digivalt-init`
|
|
20
|
+
`digivalt-init` preflights your app and patches safe script cases automatically. In a standard Vite app, it should add or update the DigiValt build wiring for you.
|
|
21
21
|
|
|
22
22
|
Still verify your `package.json` after running init. The expected result is:
|
|
23
23
|
|
|
@@ -58,7 +58,7 @@ VITE_GRAVITY_FORMS_SECRET="your_cs_secret"
|
|
|
58
58
|
# Local Execution Mode (Set to "direct" internally so your local UI bypasses Cloudflare)
|
|
59
59
|
VITE_AUTH_MODE="direct"
|
|
60
60
|
```
|
|
61
|
-
*⚠️ **CRITICAL WARNING:**
|
|
61
|
+
*⚠️ **CRITICAL WARNING:** Never enable fixture mode (`VITE_IS_LOVABLE=true`) in `.env.local` when you need live generation. The backwards-compatible flag skips API traffic and uses committed data.*
|
|
62
62
|
|
|
63
63
|
## 🌩️ 5. Deploy Infrastructure to Cloudflare
|
|
64
64
|
Once you connect this GitHub repository to the Cloudflare Pages dashboard:
|
|
@@ -77,15 +77,21 @@ Init will:
|
|
|
77
77
|
- refresh older DigiVAlt-managed `scripts/generate.ts` files
|
|
78
78
|
- reconcile the DigiValt-managed sections inside `.env.example` and `.env.local.example`
|
|
79
79
|
- warn instead of overwriting custom build logic or custom generator scripts
|
|
80
|
-
- report stale legacy workarounds that may no longer be needed
|
|
80
|
+
- report stale legacy workarounds and local registry files that may no longer be needed
|
|
81
|
+
|
|
82
|
+
## App chrome and WordPress page bodies
|
|
83
|
+
|
|
84
|
+
Use `DigiValtProviders`, `DigiValtWpPage`, and `createContentRegistry` from `@marvalt/digivalt-core/registry`. Read static chrome content with `getAppChrome()` from `@marvalt/digivalt-core/static`.
|
|
85
|
+
|
|
86
|
+
> Build Header/Footer in the app. Read menus, logo, and copy from WordPress static data (`getAppChrome()`). Apply the app’s layout and Tailwind. Do not render the WordPress header/footer template part and do not pipe `footer.blocks` / `header.blocks` through wparser.
|
|
81
87
|
|
|
82
88
|
## 7. SEO and PageSpeed (optional)
|
|
83
89
|
|
|
84
90
|
1. Install **`react-helmet-async`** and wrap your app with **`HelmetProvider`** at the root (next to the router).
|
|
85
91
|
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
|
-
3. Import **`DigiValtSeoHead`**
|
|
87
|
-
4. **Sitemap:** **`/sitemap.xml`**
|
|
88
|
-
5. Read the full guide shipped with the package: **`node_modules/@marvalt/digivalt-core/docs/SEO-PERFORMANCE.md
|
|
92
|
+
3. Import **`DigiValtRouteSeo`** (or **`DigiValtSeoHead`** / **`JsonLd`**) from **`@marvalt/digivalt-core/seo`**; pass **`pathname`** per route.
|
|
93
|
+
4. **Sitemap / robots / prerender:** **`/sitemap.xml`** and **`/robots.txt`** are Pages Functions. **`public/sitemap-paths.json`** and **`seo-manifest.json`** are generated on `generate` / `build`. After `vite build`, run **`scripts/prerender-seo.ts`**. Edit **`public/sitemap-static-paths.json`** for app-only indexable routes. Do not add **`public/sitemap.xml`** or **`public/robots.txt`**. Per-URL meta lives in the **DigiValt SEO** WordPress plugin; site identity stays in **`VITE_SEO_*`**. Optional **`VITE_SEO_ROUTE_PREFIXES`** maps CPT REST slugs to app path prefixes. See **`docs/SEO-PERFORMANCE.md`**.
|
|
94
|
+
5. Read the full guide shipped with the package: **`node_modules/@marvalt/digivalt-core/docs/SEO-PERFORMANCE.md`**.
|
|
89
95
|
|
|
90
96
|
## 8. Analytics (Google Analytics / Matomo)
|
|
91
97
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serves /robots.txt with a Sitemap directive using VITE_SEO_SITE_URL when set.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function normalizeBase(raw: string | undefined): string | null {
|
|
6
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
7
|
+
const t = raw.trim().replace(/\/+$/, '');
|
|
8
|
+
return t || null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function onRequestGet(context: { request: Request; env: Record<string, string | undefined> }) {
|
|
12
|
+
const { request, env } = context;
|
|
13
|
+
const fromEnv = normalizeBase(env.VITE_SEO_SITE_URL || env.SITE_URL);
|
|
14
|
+
const origin = new URL(request.url).origin.replace(/\/+$/, '');
|
|
15
|
+
const base = fromEnv || origin;
|
|
16
|
+
const body = `User-agent: *
|
|
17
|
+
Allow: /
|
|
18
|
+
|
|
19
|
+
Sitemap: ${base}/sitemap.xml
|
|
20
|
+
`;
|
|
21
|
+
return new Response(body, {
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
24
|
+
'Cache-Control': 'public, max-age=300',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -1,65 +1,143 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Serves /sitemap.xml from canonical site URL + public/sitemap-paths.json (generated at build).
|
|
3
|
-
* Set VITE_SEO_SITE_URL (or SITE_URL) on Cloudflare Pages for production canonical hosts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Serves /sitemap.xml from canonical site URL + public/sitemap-paths.json (generated at build).
|
|
3
|
+
* Set VITE_SEO_SITE_URL (or SITE_URL) on Cloudflare Pages for production canonical hosts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const IMAGE_NS = 'http://www.google.com/schemas/sitemap-image/1.1';
|
|
7
|
+
|
|
8
|
+
function normalizeBase(raw: string | undefined): string | null {
|
|
9
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
10
|
+
const t = raw.trim().replace(/\/+$/, '');
|
|
11
|
+
return t || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function escapeXml(s: string): string {
|
|
15
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SitemapUrl = { path: string; lastmod?: string; image?: string };
|
|
19
|
+
|
|
20
|
+
function asUrls(data: unknown): SitemapUrl[] {
|
|
21
|
+
if (!data || typeof data !== 'object') return [];
|
|
22
|
+
const rec = data as { urls?: unknown; paths?: unknown };
|
|
23
|
+
if (Array.isArray(rec.urls)) {
|
|
24
|
+
return rec.urls
|
|
25
|
+
.map((u) => {
|
|
26
|
+
if (typeof u === 'string' && u.startsWith('/')) return { path: u };
|
|
27
|
+
if (u && typeof u === 'object' && typeof (u as { path?: unknown }).path === 'string') {
|
|
28
|
+
const row = u as { path: string; lastmod?: unknown; image?: unknown };
|
|
29
|
+
return {
|
|
30
|
+
path: row.path,
|
|
31
|
+
lastmod: typeof row.lastmod === 'string' ? row.lastmod : undefined,
|
|
32
|
+
image: typeof row.image === 'string' ? row.image : undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
})
|
|
37
|
+
.filter((u): u is SitemapUrl => !!u && u.path.startsWith('/'));
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(rec.paths)) {
|
|
40
|
+
return rec.paths.filter((p): p is string => typeof p === 'string' && p.startsWith('/')).map((path) => ({ path }));
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchJson(request: Request, pathname: string): Promise<unknown> {
|
|
46
|
+
const jsonUrl = new URL(pathname, request.url);
|
|
47
|
+
const res = await fetch(jsonUrl.toString());
|
|
48
|
+
if (!res.ok) return null;
|
|
49
|
+
return res.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function loadUrls(request: Request): Promise<SitemapUrl[]> {
|
|
53
|
+
try {
|
|
54
|
+
const data = await fetchJson(request, '/sitemap-paths.json');
|
|
55
|
+
if (!data || typeof data !== 'object') return [{ path: '/' }];
|
|
56
|
+
const rec = data as { index?: unknown; parts?: unknown };
|
|
57
|
+
if (rec.index === true && typeof rec.parts === 'number' && rec.parts > 1) {
|
|
58
|
+
const partParam = new URL(request.url).searchParams.get('part');
|
|
59
|
+
if (partParam === null) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const part = Number(partParam);
|
|
63
|
+
if (!Number.isInteger(part) || part < 0 || part >= rec.parts) return [{ path: '/' }];
|
|
64
|
+
const shard = await fetchJson(request, `/sitemap-paths-${part}.json`);
|
|
65
|
+
const urls = asUrls(shard);
|
|
66
|
+
return urls.length ? urls : [{ path: '/' }];
|
|
67
|
+
}
|
|
68
|
+
const urls = asUrls(data);
|
|
69
|
+
return urls.length ? urls : [{ path: '/' }];
|
|
70
|
+
} catch {
|
|
71
|
+
return [{ path: '/' }];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadIndexParts(request: Request): Promise<number | null> {
|
|
76
|
+
try {
|
|
77
|
+
const data = await fetchJson(request, '/sitemap-paths.json');
|
|
78
|
+
if (data && typeof data === 'object') {
|
|
79
|
+
const rec = data as { index?: unknown; parts?: unknown };
|
|
80
|
+
if (rec.index === true && typeof rec.parts === 'number' && rec.parts > 1) return rec.parts;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
/* ignore */
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function locFor(base: string, p: string): string {
|
|
89
|
+
return p === '/' ? base : `${base}${p.startsWith('/') ? p : `/${p}`}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildUrlset(base: string, urls: SitemapUrl[]): string {
|
|
93
|
+
const lines = urls.map((u) => {
|
|
94
|
+
const loc = locFor(base, u.path);
|
|
95
|
+
const lastmod = u.lastmod ? `\n <lastmod>${escapeXml(u.lastmod)}</lastmod>` : '';
|
|
96
|
+
const image = u.image
|
|
97
|
+
? `\n <image:image>\n <image:loc>${escapeXml(u.image)}</image:loc>\n </image:image>`
|
|
98
|
+
: '';
|
|
99
|
+
return ` <url>\n <loc>${escapeXml(loc)}</loc>${lastmod}${image}\n </url>`;
|
|
100
|
+
});
|
|
101
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
102
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="${IMAGE_NS}">
|
|
103
|
+
${lines.join('\n')}
|
|
104
|
+
</urlset>`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildIndex(base: string, parts: number): string {
|
|
108
|
+
const items = [];
|
|
109
|
+
for (let i = 0; i < parts; i++) {
|
|
110
|
+
items.push(` <sitemap><loc>${escapeXml(`${base}/sitemap.xml?part=${i}`)}</loc></sitemap>`);
|
|
111
|
+
}
|
|
112
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
113
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
114
|
+
${items.join('\n')}
|
|
115
|
+
</sitemapindex>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function onRequestGet(context: { request: Request; env: Record<string, string | undefined> }) {
|
|
119
|
+
const { request, env } = context;
|
|
120
|
+
const fromEnv = normalizeBase(env.VITE_SEO_SITE_URL || env.SITE_URL);
|
|
121
|
+
const origin = new URL(request.url).origin.replace(/\/+$/, '');
|
|
122
|
+
const base = fromEnv || origin;
|
|
123
|
+
const part = new URL(request.url).searchParams.get('part');
|
|
124
|
+
|
|
125
|
+
const indexParts = await loadIndexParts(request);
|
|
126
|
+
if (indexParts && part === null) {
|
|
127
|
+
return new Response(buildIndex(base, indexParts), {
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
130
|
+
'Cache-Control': 'public, max-age=300',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const urls = await loadUrls(request);
|
|
136
|
+
const xml = buildUrlset(base, urls.length ? urls : [{ path: '/' }]);
|
|
137
|
+
return new Response(xml, {
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
140
|
+
'Cache-Control': 'public, max-age=300',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import { generateAllData, generateSitemapPathsSafe } from '@marvalt/digivalt-core/generators';
|
|
1
|
+
import { generateAllData, generateSitemapPathsSafe, generateSeoManifestSafe } from '@marvalt/digivalt-core/generators';
|
|
2
2
|
|
|
3
3
|
async function runGenerators() {
|
|
4
|
-
console.log('🚀 Starting DigiVAlt static data generation
|
|
5
|
-
|
|
6
|
-
if (process.env.VITE_IS_LOVABLE === 'true') {
|
|
7
|
-
console.log(
|
|
8
|
-
'⚡ Lovable Environment Detected (VITE_IS_LOVABLE). Skipping static data fetch; refreshing sitemap paths from committed JSON.'
|
|
9
|
-
);
|
|
10
|
-
await generateSitemapPathsSafe();
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
4
|
+
console.log('🚀 Starting DigiVAlt static data generation...');
|
|
13
5
|
|
|
14
6
|
try {
|
|
15
7
|
await generateAllData();
|
|
@@ -17,6 +9,7 @@ async function runGenerators() {
|
|
|
17
9
|
} catch (error) {
|
|
18
10
|
console.error('❌ Data fetching encountered an error, but continuing build gracefully:', error);
|
|
19
11
|
await generateSitemapPathsSafe();
|
|
12
|
+
await generateSeoManifestSafe();
|
|
20
13
|
}
|
|
21
14
|
}
|
|
22
15
|
|