Skip to content

Commit a38814c

Browse files
authored
Merge pull request #26 from nightt5879/nightt5879/fix-site-url-normalization-1.3.5
fix: normalize production site URLs for 1.3.5
2 parents f6e0060 + 7293be9 commit a38814c

9 files changed

Lines changed: 86 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [1.3.5] - 2026-05-03
8+
9+
### Changed
10+
- Bumped the package version from `1.3.4` to `1.3.5` for production Git build URL hardening.
11+
- Normalized site URL inputs so missing `https://` is treated as HTTPS instead of breaking static route generation.
12+
13+
### Fixed
14+
- Prevented invalid `PUBLIC_SITE_URL` values from crashing Open Graph and canonical URL generation during Cloudflare Pages production builds.
15+
- Kept CMS config and smoke checks aligned with the same URL normalization rules.
16+
717
## [1.3.4] - 2026-05-03
818

919
### Added

README_EN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ venue: "Conference Name"
228228
- `1.3.2`: CMS stability patch that adds stable legacy news slugs, removes CMS test residue, and tightens news slug validation
229229
- `1.3.3`: build-log cleanup that clears Astro content cache before builds to remove duplicate news id warnings
230230
- `1.3.4`: Cloudflare Pages Git build stability patch that pins Node.js and records the Pages output directory in repo config
231+
- `1.3.5`: production Git build hardening that normalizes malformed site URL variables before static rendering
231232

232233
## License
233234

docs/cloudflare-pages-build-settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The repository pins the Node.js version in `.node-version`, and the Pages projec
1818
- `CMS_OAUTH_BASE_URL=https://doubleducklab-cms-oauth.<account>.workers.dev`
1919
- `PUBLIC_SITE_URL=https://doubleducklab.pages.dev`
2020

21-
`NODE_VERSION` is a build-environment pin, not a CMS secret. It is listed with Pages secrets only because Wrangler manages Pages variables through `wrangler pages secret`.
21+
`PUBLIC_SITE_URL` must be an absolute site URL. The build now tolerates a missing `https://` prefix, but the stored Pages value should still include the full `https://` URL. `NODE_VERSION` is a build-environment pin, not a CMS secret. It is listed with Pages secrets only because Wrangler manages Pages variables through `wrangler pages secret`.
2222

2323
## Verification
2424

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "double-duck-lab",
33
"type": "module",
4-
"version": "1.3.4",
4+
"version": "1.3.5",
55
"private": true,
66
"scripts": {
77
"dev": "astro dev",

scripts/site-url.mjs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,22 @@ import { loadEnv } from 'vite';
33
export const DEFAULT_SITE_URL = 'https://doubleducklab.com/';
44

55
export function normalizeSiteUrl(value) {
6-
const site = (value || DEFAULT_SITE_URL).trim();
7-
return site.endsWith('/') ? site : `${site}/`;
6+
const raw = `${value || DEFAULT_SITE_URL}`.trim().replace(/^['"]|['"]$/g, '');
7+
const candidate = /^[a-z][a-z\d+\-.]*:\/\//i.test(raw) ? raw : `https://${raw}`;
8+
9+
try {
10+
const url = new URL(candidate);
11+
if (!['http:', 'https:'].includes(url.protocol)) {
12+
throw new Error(`Unsupported site URL protocol: ${url.protocol}`);
13+
}
14+
15+
url.search = '';
16+
url.hash = '';
17+
const site = url.toString();
18+
return site.endsWith('/') ? site : `${site}/`;
19+
} catch {
20+
return DEFAULT_SITE_URL;
21+
}
822
}
923

1024
export function resolveConfiguredSiteUrl(mode = 'production') {

scripts/smoke-build.mjs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,24 @@ const publicSiteUrl = (process.env.PUBLIC_SITE_URL || '').trim();
88
const cmsBranch = (process.env.CMS_BRANCH || 'main').trim() || 'main';
99

1010
function normalizeUrl(value) {
11-
if (!value) {
11+
const raw = (value || '').trim().replace(/^['"]|['"]$/g, '');
12+
if (!raw) {
1213
return '';
1314
}
1415

15-
return value.replace(/\/$/, '');
16+
const candidate = /^[a-z][a-z\d+\-.]*:\/\//i.test(raw) ? raw : `https://${raw}`;
17+
try {
18+
const url = new URL(candidate);
19+
if (!['http:', 'https:'].includes(url.protocol)) {
20+
return '';
21+
}
22+
23+
url.search = '';
24+
url.hash = '';
25+
return url.toString().replace(/\/$/, '');
26+
} catch {
27+
return '';
28+
}
1629
}
1730

1831
function getUrlHostname(value) {
@@ -21,7 +34,7 @@ function getUrlHostname(value) {
2134
}
2235

2336
try {
24-
return new URL(`${value}/`).hostname;
37+
return new URL(value).hostname;
2538
} catch {
2639
return '';
2740
}

src/utils/cms-config.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,29 @@ export type CmsRuntimeConfig = {
1616
};
1717

1818
function normalizeUrl(value: string | undefined, fallback = '') {
19-
const normalized = (value || fallback).trim();
20-
if (!normalized) {
19+
const raw = (value || fallback).trim().replace(/^['"]|['"]$/g, '');
20+
if (!raw) {
2121
return '';
2222
}
2323

24-
return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
24+
const candidate = /^[a-z][a-z\d+\-.]*:\/\//i.test(raw) ? raw : `https://${raw}`;
25+
try {
26+
const url = new URL(candidate);
27+
if (!['http:', 'https:'].includes(url.protocol)) {
28+
return '';
29+
}
30+
31+
url.search = '';
32+
url.hash = '';
33+
return url.toString().replace(/\/$/, '');
34+
} catch {
35+
return '';
36+
}
2537
}
2638

2739
function getUrlHostname(value: string) {
2840
try {
29-
return new URL(`${value}/`).hostname;
41+
return new URL(value).hostname;
3042
} catch {
3143
return '';
3244
}

src/utils/seo.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,32 @@ export function buildDefaultLocaleAlternates(pathname: string): LocaleAlternates
5353
}
5454

5555
export function toAbsoluteUrl(pathname: string, site: URL | string | undefined) {
56-
const siteUrl = site instanceof URL ? site : new URL(site || DEFAULT_SITE_URL);
56+
const siteUrl = normalizeSiteUrl(site);
5757
return new URL(pathname, siteUrl).toString();
5858
}
5959

60+
export function normalizeSiteUrl(site: URL | string | undefined) {
61+
if (site instanceof URL) {
62+
return site;
63+
}
64+
65+
const raw = `${site || DEFAULT_SITE_URL}`.trim().replace(/^['"]|['"]$/g, '');
66+
const candidate = /^[a-z][a-z\d+\-.]*:\/\//i.test(raw) ? raw : `https://${raw}`;
67+
68+
try {
69+
const url = new URL(candidate);
70+
if (!['http:', 'https:'].includes(url.protocol)) {
71+
throw new Error(`Unsupported site URL protocol: ${url.protocol}`);
72+
}
73+
74+
url.search = '';
75+
url.hash = '';
76+
return url;
77+
} catch {
78+
return new URL(DEFAULT_SITE_URL);
79+
}
80+
}
81+
6082
export function summarizeText(value: string | undefined, maxLength = 160) {
6183
const plain = (value || '')
6284
.replace(/```[\s\S]*?```/g, ' ')

0 commit comments

Comments
 (0)