storyloco 2.8.0 → 2.9.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/README.md
CHANGED
|
@@ -394,7 +394,7 @@ The plugin will:
|
|
|
394
394
|
|
|
395
395
|
- Fetch redirect entries from your Storyblok datasource
|
|
396
396
|
- Bake them into the build as a fallback
|
|
397
|
-
- Support exact matches and wildcard patterns
|
|
397
|
+
- Support exact matches and wildcard patterns, with or without a trailing slash (SvelteKit strips it before hooks run, so `/old/` matches a request for `/old`)
|
|
398
398
|
- Handle both internal and external redirects
|
|
399
399
|
|
|
400
400
|
Redirects stay live without a rebuild: the handle fetches the datasource on a server's first request, then serves from memory and refreshes in the background once the list is a minute old. If Storyblok can't be reached, it keeps the last list it had, or the one baked in at build.
|
package/package.json
CHANGED
|
@@ -9,6 +9,15 @@ function normalize_path(pathname) {
|
|
|
9
9
|
return "/";
|
|
10
10
|
return pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* How paths are compared: leading slash, no trailing slash, query kept. SvelteKit strips a trailing
|
|
14
|
+
* slash before hooks run, so `/old/` has to match a request for `/old`.
|
|
15
|
+
*/
|
|
16
|
+
function match_key(path) {
|
|
17
|
+
const i = path.indexOf("?");
|
|
18
|
+
const [pathname, query] = i < 0 ? [path, ""] : [path.slice(0, i), path.slice(i)];
|
|
19
|
+
return normalize_path(pathname).replace(/(.)\/+$/, "$1") + query;
|
|
20
|
+
}
|
|
12
21
|
/** Absolute URL check (e.g. https://..., http://..., //cdn...) */
|
|
13
22
|
function is_absolute_url(value) {
|
|
14
23
|
return /^https?:\/\//i.test(value) || value.startsWith("//");
|
|
@@ -30,7 +39,7 @@ function build_rules(redirects) {
|
|
|
30
39
|
if (key.includes("*")) {
|
|
31
40
|
try {
|
|
32
41
|
rules.wildcards.push({
|
|
33
|
-
matcher: match(to_src_pattern(key), { decode: decodeURIComponent, end: true }),
|
|
42
|
+
matcher: match(to_src_pattern(match_key(key)), { decode: decodeURIComponent, end: true }),
|
|
34
43
|
builder: compile(to_dst_pattern(value), { encode: (x) => x }),
|
|
35
44
|
});
|
|
36
45
|
}
|
|
@@ -39,8 +48,10 @@ function build_rules(redirects) {
|
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
else {
|
|
42
|
-
rules.exact.
|
|
43
|
-
|
|
51
|
+
const existing = rules.exact.get(match_key(key));
|
|
52
|
+
if (existing !== undefined && existing !== value)
|
|
53
|
+
logger.warn(`${key} -> ${value} replaces ${existing}: keys match with or without a trailing slash`);
|
|
54
|
+
rules.exact.set(match_key(key), value);
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
return rules;
|
|
@@ -90,11 +101,10 @@ function warn(err) {
|
|
|
90
101
|
async function resolve_redirect(url) {
|
|
91
102
|
const { exact, wildcards } = await current_rules();
|
|
92
103
|
try {
|
|
93
|
-
const source =
|
|
94
|
-
const source_plain =
|
|
95
|
-
const lookup = (key) => exact.get(key) ?? exact.get(key.startsWith("/") ? key.slice(1) : key);
|
|
104
|
+
const source = match_key(url.pathname + url.search);
|
|
105
|
+
const source_plain = match_key(url.pathname);
|
|
96
106
|
// Prefer exact including query, then plain path
|
|
97
|
-
let target =
|
|
107
|
+
let target = exact.get(source) ?? exact.get(source_plain) ?? null;
|
|
98
108
|
if (!target) {
|
|
99
109
|
for (const { matcher, builder } of wildcards) {
|
|
100
110
|
const m = matcher(source_plain);
|
|
@@ -109,7 +119,8 @@ async function resolve_redirect(url) {
|
|
|
109
119
|
const result = is_absolute_url(target) ? target : normalize_path(target);
|
|
110
120
|
// If source included a query and the target is a relative path without its own query,
|
|
111
121
|
// we do NOT automatically forward the original query. The map should specify it.
|
|
112
|
-
|
|
122
|
+
const result_key = match_key(result);
|
|
123
|
+
if (result_key !== source && result_key !== source_plain)
|
|
113
124
|
return result;
|
|
114
125
|
}
|
|
115
126
|
return null;
|
|
@@ -210,15 +210,18 @@ interface Link {
|
|
|
210
210
|
linktype?: string;
|
|
211
211
|
url?: string;
|
|
212
212
|
cached_url?: string;
|
|
213
|
+
anchor?: string;
|
|
214
|
+
email?: string;
|
|
213
215
|
story?: {
|
|
214
216
|
url?: string;
|
|
215
217
|
[key: string]: unknown;
|
|
216
218
|
};
|
|
217
219
|
}
|
|
218
220
|
/**
|
|
219
|
-
* Resolve a multilink to an href
|
|
220
|
-
*
|
|
221
|
-
*
|
|
221
|
+
* Resolve a multilink to an href, or `undefined` when the field links nowhere (so an `<a>`
|
|
222
|
+
* renders without one). Story links become root-relative with their anchor (`#section`);
|
|
223
|
+
* email links become `mailto:`; URL and asset links pass through. Prefers `story.url` over
|
|
224
|
+
* `cached_url`, which goes stale when a target moves, so pair with `resolve_links: "url"`.
|
|
222
225
|
*/
|
|
223
226
|
export declare function href(link?: Link): string | undefined;
|
|
224
227
|
/**
|
|
@@ -157,16 +157,23 @@ export function is_component(story, component) {
|
|
|
157
157
|
return story?.content.component === component;
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
|
-
* Resolve a multilink to an href
|
|
161
|
-
*
|
|
162
|
-
*
|
|
160
|
+
* Resolve a multilink to an href, or `undefined` when the field links nowhere (so an `<a>`
|
|
161
|
+
* renders without one). Story links become root-relative with their anchor (`#section`);
|
|
162
|
+
* email links become `mailto:`; URL and asset links pass through. Prefers `story.url` over
|
|
163
|
+
* `cached_url`, which goes stale when a target moves, so pair with `resolve_links: "url"`.
|
|
163
164
|
*/
|
|
164
165
|
export function href(link) {
|
|
165
166
|
if (!link)
|
|
166
167
|
return undefined;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
if (link.linktype === "email") {
|
|
169
|
+
const email = link.email || link.url;
|
|
170
|
+
return email ? `mailto:${email.replace(/^mailto:/, "")}` : undefined;
|
|
171
|
+
}
|
|
172
|
+
if (link.linktype !== "story")
|
|
173
|
+
return link.url || link.cached_url || undefined;
|
|
174
|
+
const path = (link.story?.url || link.cached_url || "").replace(/^\/+/, "");
|
|
175
|
+
const anchor = link.anchor ? `#${link.anchor.replace(/^#/, "")}` : "";
|
|
176
|
+
return path ? `/${path}${anchor}` : anchor || undefined;
|
|
170
177
|
}
|
|
171
178
|
/**
|
|
172
179
|
* Turn a Storyblok error into a SvelteKit one, for `.catch(handle_error)`.
|