routerino 2.4.0 → 2.5.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 +61 -5
- package/dist/routerino.js +365 -273
- package/dist/routerino.umd.cjs +1 -1
- package/package.json +1 -1
- package/routerino-forge.js +10 -3
- package/types/routerino.d.ts +12 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Routerino
|
|
2
2
|
|
|
3
|
-
> A lightweight, SEO-optimized React router for modern websites and applications
|
|
3
|
+
> A lightweight, SEO-optimized React router & SSG for modern websites and applications
|
|
4
4
|
|
|
5
5
|
For teams who want SPA simplicity with search-friendly static HTML, Open Graph previews, and **no framework lock-in.**
|
|
6
6
|
|
|
@@ -129,7 +129,18 @@ export const routes = [
|
|
|
129
129
|
/>;
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
Links are just standard HTML anchor tags. No need to use special `<Link>` components—you can use whatever components or design system you like. For example: <a href="/some-page/">a link</a> is perfectly valid. This is very handy for markdown-based content. With standard link support in Routerino, you won't need to [transform your markdown content with custom React components](https://github.com/remarkjs/react-markdown?tab=readme-ov-file#appendix-b-components).
|
|
132
|
+
Links are just standard HTML anchor tags. No need to use special `<Link>` components—you can use whatever components or design system you like. For example: <a href="/some-page/">a link</a> is perfectly valid. This is very handy for markdown-based content. With standard link support in Routerino, you won't need to [transform your markdown content with custom React components](https://github.com/remarkjs/react-markdown?tab=readme-ov-file#appendix-b-components).
|
|
133
|
+
|
|
134
|
+
Routerino handles same-origin anchor clicks via SPA navigation. The browser handles the rest natively, including:
|
|
135
|
+
|
|
136
|
+
- **Cross-origin links** (different domain)
|
|
137
|
+
- **Non-HTTP schemes** (mailto:, tel:, javascript:, etc.)
|
|
138
|
+
- **Modified clicks** (Ctrl/Cmd+click, Shift+click, Alt+click, right-click)
|
|
139
|
+
- **`target="_blank"`** links
|
|
140
|
+
- **`download`** attribute links
|
|
141
|
+
- **`rel="external"`** links
|
|
142
|
+
- **File extension links** — PDFs, ZIPs, images, fonts, videos, JSON, XML, and 50+ other file types are recognized automatically
|
|
143
|
+
- **Custom patterns** — use the [`ignorePatterns`](#ignorepatterns-string) prop for additional exclusions (e.g., `/api/`, `/legacy/`)
|
|
133
144
|
|
|
134
145
|
### Programmatic Navigation
|
|
135
146
|
|
|
@@ -224,6 +235,7 @@ All of these are optional, so it's easy to get started with nothing but a bare-b
|
|
|
224
235
|
| [imageUrl](#imageurl-string) | string | Default image URL for sharing | `null` |
|
|
225
236
|
| [touchIconUrl](#touchiconurl-string) | string | Image URL for PWA homescreen icon | `null` |
|
|
226
237
|
| [debug](#debug-boolean) | boolean | Enable debug mode | `false` |
|
|
238
|
+
| [ignorePatterns](#ignorepatterns-string) | string[] | URL patterns to skip SPA routing | `[]` |
|
|
227
239
|
|
|
228
240
|
##### `title`: string;
|
|
229
241
|
|
|
@@ -300,7 +312,9 @@ Default: `false`
|
|
|
300
312
|
|
|
301
313
|
##### `baseUrl`: string;
|
|
302
314
|
|
|
303
|
-
The base URL to use for canonical tags and og:url meta tags. If not provided, uses `window.location.origin`.
|
|
315
|
+
The base URL to use for canonical tags and og:url meta tags. If not provided, uses `window.location.origin`.
|
|
316
|
+
|
|
317
|
+
This is useful when your site is accessible from multiple domains (for example, both example.com and example.net point to the same app). Set it to the preferred domain and all canonical and og:url tags will consistently point there. Also helpful for pinning to the production URL in development or staging environments.
|
|
304
318
|
|
|
305
319
|
**Important:** The baseUrl must NOT end with a trailing slash (`/`). Correct example: `"https://example.com"`
|
|
306
320
|
|
|
@@ -338,6 +352,21 @@ Example debug output:
|
|
|
338
352
|
|
|
339
353
|
Default: `false`
|
|
340
354
|
|
|
355
|
+
##### `ignorePatterns`: string[];
|
|
356
|
+
|
|
357
|
+
An array of regex pattern strings to match against link hrefs. Any same-origin link matching one of these patterns will **not** be intercepted by the SPA router — the browser will handle the navigation natively instead.
|
|
358
|
+
|
|
359
|
+
Patterns are tested case-insensitively against the full resolved href (including origin).
|
|
360
|
+
|
|
361
|
+
```jsx
|
|
362
|
+
// Skip API endpoints and a legacy section
|
|
363
|
+
<Routerino routes={routes} ignorePatterns={["/api/", "/admin/legacy/"]} />
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Routerino also has a built-in list of file extensions that are automatically skipped: `.pdf`, `.zip`, `.docx`, `.png`, `.jpg`, `.svg`, `.mp4`, `.json`, `.xml`, `.csv`, `.txt`, `.ico`, `.woff2`, `.woff`, `.ttf`, `.mp3`, `.wav`, `.epub`, `.map`, `.css`, `.js`, `.wasm`, and many more. The `ignorePatterns` prop is for additional custom patterns beyond these defaults.
|
|
367
|
+
|
|
368
|
+
Default: `[]`
|
|
369
|
+
|
|
341
370
|
#### RouteConfig props
|
|
342
371
|
|
|
343
372
|
There is a default RouteConfig that will be loaded if you don't specify any routes. The default route is a basic template that can confirm your app is working and routing is good to go.
|
|
@@ -402,6 +431,31 @@ An array of HeadTag objects that can be added to the route to manage meta tags,
|
|
|
402
431
|
|
|
403
432
|
- `target` (string): The "target" attribute of the tag. Defines where to open the linked resource.
|
|
404
433
|
|
|
434
|
+
- `innerHTML` (string): Inner HTML content for tags that require a closing tag, such as `<script>` and `<style>`. When provided, the tag is rendered as `<tag attrs>innerHTML</tag>` instead of a self-closing element. This enables structured data (JSON-LD), inline CSS, and other tag-body content.
|
|
435
|
+
|
|
436
|
+
##### Structured Data Example
|
|
437
|
+
|
|
438
|
+
```jsx
|
|
439
|
+
{
|
|
440
|
+
path: "/about/",
|
|
441
|
+
element: <AboutPage />,
|
|
442
|
+
tags: [{
|
|
443
|
+
tag: "script",
|
|
444
|
+
type: "application/ld+json",
|
|
445
|
+
innerHTML: JSON.stringify({
|
|
446
|
+
"@context": "https://schema.org",
|
|
447
|
+
"@type": "BreadcrumbList",
|
|
448
|
+
itemListElement: [
|
|
449
|
+
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/" },
|
|
450
|
+
{ "@type": "ListItem", "position": 2, "name": "About", "item": "https://example.com/about/" },
|
|
451
|
+
],
|
|
452
|
+
}),
|
|
453
|
+
}],
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
This works both at runtime (via `updateHeadTag`) and during static site generation (via the forge plugin).
|
|
458
|
+
|
|
405
459
|
### Using the `useRouterino` Hook
|
|
406
460
|
|
|
407
461
|
The `useRouterino` hook provides access to router state from any child component. This is the primary way to access route information and update head tags dynamically.
|
|
@@ -717,6 +771,8 @@ With Prerender (SPA):
|
|
|
717
771
|
|
|
718
772
|
You can override the default with `useTrailingSlash={false}` if you prefer URLs without trailing slashes. Either way, Routerino ensures search engines see consistent, canonical URLs.
|
|
719
773
|
|
|
774
|
+
**Multi-domain deployments**: If your site is served from multiple domains, set `baseUrl` to the preferred domain to keep all canonical and og:url tags pointing to one place. For example, `<Routerino baseUrl="https://example.com" routes={routes} />` ensures search engines see one canonical domain even when the same page is reachable at example.net.
|
|
775
|
+
|
|
720
776
|
### Sitemap Generation
|
|
721
777
|
|
|
722
778
|
- Automate the creation of `sitemap.xml` and `robots.txt` during your build process with Routerino Forge.
|
|
@@ -808,14 +864,14 @@ This creates an engaging Twitter preview with a large image, title, and descript
|
|
|
808
864
|
### Additional SEO Considerations
|
|
809
865
|
|
|
810
866
|
- Use semantic HTML elements in your components for better content structure.
|
|
811
|
-
- Implement structured data (JSON-LD) where applicable to enhance rich snippets in search results.
|
|
867
|
+
- Implement structured data (JSON-LD) where applicable to enhance rich snippets in search results. Use the `innerHTML` property on a `<script type="application/ld+json">` head tag (see the [Structured Data Example](#structured-data-example) above).
|
|
812
868
|
- Ensure your site is mobile-friendly and loads quickly for better search engine rankings.
|
|
813
869
|
|
|
814
870
|
By following these practices, you'll improve your site's SEO performance and social media presence when using Routerino.
|
|
815
871
|
|
|
816
872
|
### Hash Links
|
|
817
873
|
|
|
818
|
-
Routerino supports standard `<a href="/page#section">` links for SPA navigation
|
|
874
|
+
Routerino supports standard `<a href="/page#section">` links for SPA navigation. After React renders the new page, it finds the element with the matching `id` and scrolls it into view.
|
|
819
875
|
|
|
820
876
|
**Sticky headers**: If your site has a fixed header, use the CSS [`scroll-margin-top`](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-margin-top) property to offset the scroll target so content isn't hidden behind the header:
|
|
821
877
|
|
package/dist/routerino.js
CHANGED
|
@@ -1,385 +1,476 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { useState as
|
|
3
|
-
import
|
|
1
|
+
import { jsx as m, jsxs as q, Fragment as V } from "react/jsx-runtime";
|
|
2
|
+
import { useState as M, useEffect as _, useMemo as X, createContext as Z, Component as J, useContext as Q } from "react";
|
|
3
|
+
import t from "prop-types";
|
|
4
4
|
const Y = [480, 800, 1200, 1920], ee = "(max-width: 480px) 100vw, (max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px", H = /* @__PURE__ */ new Map();
|
|
5
5
|
function te(i, e = "") {
|
|
6
|
-
const
|
|
7
|
-
return !!(
|
|
6
|
+
const c = i.toLowerCase(), n = e.toLowerCase();
|
|
7
|
+
return !!(c.includes("hero") || c.includes("banner") || n.includes("hero") || n.includes("banner") || n.includes("h-screen") || n.includes("h-full"));
|
|
8
8
|
}
|
|
9
|
-
function
|
|
10
|
-
const n = i.replace(/\.(jpe?g|png|webp)$/i, ""),
|
|
11
|
-
return e.map((
|
|
9
|
+
function O(i, e, c = null) {
|
|
10
|
+
const n = i.replace(/\.(jpe?g|png|webp)$/i, ""), f = c ? `.${c}` : i.match(/\.(jpe?g|png|webp)$/i)?.[0] || ".jpg";
|
|
11
|
+
return e.map((l) => `${n}-${l}w${f} ${l}w`).join(", ");
|
|
12
12
|
}
|
|
13
13
|
function oe(i) {
|
|
14
14
|
const {
|
|
15
15
|
src: e = "",
|
|
16
|
-
alt:
|
|
16
|
+
alt: c = "",
|
|
17
17
|
priority: n,
|
|
18
|
-
widths:
|
|
19
|
-
sizes:
|
|
20
|
-
className:
|
|
21
|
-
style:
|
|
22
|
-
width:
|
|
23
|
-
height:
|
|
24
|
-
loading:
|
|
25
|
-
decoding:
|
|
26
|
-
fetchpriority:
|
|
27
|
-
...
|
|
28
|
-
} = i || {},
|
|
18
|
+
widths: f = Y,
|
|
19
|
+
sizes: l = ee,
|
|
20
|
+
className: u = "",
|
|
21
|
+
style: F = {},
|
|
22
|
+
width: v,
|
|
23
|
+
height: $,
|
|
24
|
+
loading: A,
|
|
25
|
+
decoding: j = "async",
|
|
26
|
+
fetchpriority: d,
|
|
27
|
+
...k
|
|
28
|
+
} = i || {}, U = typeof window > "u", S = !U && typeof document < "u" && typeof HTMLElement < "u" && document.createElement("div") instanceof HTMLElement, x = S && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"), W = n ?? te(e, u), a = A || (W ? "eager" : "lazy"), P = d || (W ? "high" : void 0), [E, C] = M(null);
|
|
29
29
|
_(() => {
|
|
30
|
-
if (!
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
width:
|
|
35
|
-
height:
|
|
30
|
+
if (!S || x || !e) return;
|
|
31
|
+
const r = new window.Image();
|
|
32
|
+
r.onload = () => {
|
|
33
|
+
C({
|
|
34
|
+
width: r.naturalWidth,
|
|
35
|
+
height: r.naturalHeight
|
|
36
36
|
});
|
|
37
|
-
},
|
|
38
|
-
}, [e,
|
|
39
|
-
const
|
|
37
|
+
}, r.src = e;
|
|
38
|
+
}, [e, S, x]);
|
|
39
|
+
const s = X(() => E ? f.filter((r) => E.width >= r) : f, [E, f]), [B, D] = M(null);
|
|
40
40
|
_(() => {
|
|
41
|
-
if (!
|
|
42
|
-
let
|
|
41
|
+
if (!S || x) return;
|
|
42
|
+
let r = !1;
|
|
43
43
|
return (async () => {
|
|
44
|
-
const
|
|
45
|
-
for (const
|
|
46
|
-
const
|
|
47
|
-
if (H.has(
|
|
48
|
-
H.get(
|
|
44
|
+
const y = e.replace(/\.(jpe?g|png|webp)$/i, ""), T = e.match(/\.(jpe?g|png|webp)$/i)?.[0] || ".jpg", R = [];
|
|
45
|
+
for (const L of s) {
|
|
46
|
+
const I = `${y}-${L}w${T}`;
|
|
47
|
+
if (H.has(I)) {
|
|
48
|
+
H.get(I) && R.push(L);
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
|
-
const
|
|
53
|
-
H.set(
|
|
52
|
+
const K = await fetch(I, { method: "HEAD" });
|
|
53
|
+
H.set(I, K.ok), K.ok && R.push(L);
|
|
54
54
|
} catch {
|
|
55
|
-
H.set(
|
|
55
|
+
H.set(I, !1);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
r || D(
|
|
59
|
+
R.length > 0 ? R : s
|
|
60
60
|
);
|
|
61
61
|
})(), () => {
|
|
62
|
-
|
|
62
|
+
r = !0;
|
|
63
63
|
};
|
|
64
|
-
}, [e,
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const
|
|
64
|
+
}, [e, s, S, x]);
|
|
65
|
+
const z = B ?? s, b = {};
|
|
66
|
+
v != null && (b.width = v), $ != null && (b.height = $), E && v == null && $ == null && (b.width = E.width, b.height = E.height);
|
|
67
|
+
const o = {
|
|
68
68
|
maxWidth: "100%",
|
|
69
69
|
height: "auto",
|
|
70
|
-
...
|
|
70
|
+
...F
|
|
71
71
|
};
|
|
72
|
-
if (
|
|
73
|
-
return /* @__PURE__ */
|
|
72
|
+
if (x)
|
|
73
|
+
return /* @__PURE__ */ m(
|
|
74
74
|
"img",
|
|
75
75
|
{
|
|
76
76
|
src: e,
|
|
77
|
-
alt:
|
|
78
|
-
loading:
|
|
79
|
-
decoding:
|
|
77
|
+
alt: c,
|
|
78
|
+
loading: a,
|
|
79
|
+
decoding: j,
|
|
80
80
|
fetchPriority: P,
|
|
81
|
-
className:
|
|
82
|
-
style:
|
|
83
|
-
...
|
|
84
|
-
...
|
|
81
|
+
className: u,
|
|
82
|
+
style: o,
|
|
83
|
+
...b,
|
|
84
|
+
...k
|
|
85
85
|
}
|
|
86
86
|
);
|
|
87
|
-
if (
|
|
88
|
-
return /* @__PURE__ */
|
|
89
|
-
/* @__PURE__ */
|
|
87
|
+
if (U)
|
|
88
|
+
return /* @__PURE__ */ q("picture", { "data-routerino-image": "true", "data-original-src": e, children: [
|
|
89
|
+
/* @__PURE__ */ m(
|
|
90
90
|
"source",
|
|
91
91
|
{
|
|
92
|
-
srcSet:
|
|
92
|
+
srcSet: O(e, f, "webp"),
|
|
93
93
|
type: "image/webp",
|
|
94
|
-
sizes:
|
|
94
|
+
sizes: l
|
|
95
95
|
}
|
|
96
96
|
),
|
|
97
|
-
/* @__PURE__ */
|
|
97
|
+
/* @__PURE__ */ m(
|
|
98
98
|
"img",
|
|
99
99
|
{
|
|
100
100
|
src: e,
|
|
101
|
-
alt:
|
|
102
|
-
srcSet:
|
|
103
|
-
sizes:
|
|
104
|
-
loading:
|
|
101
|
+
alt: c,
|
|
102
|
+
srcSet: O(e, f),
|
|
103
|
+
sizes: l,
|
|
104
|
+
loading: a,
|
|
105
105
|
decoding: "async",
|
|
106
106
|
fetchPriority: P,
|
|
107
|
-
className:
|
|
108
|
-
style:
|
|
109
|
-
...
|
|
110
|
-
...
|
|
107
|
+
className: u,
|
|
108
|
+
style: o,
|
|
109
|
+
...b,
|
|
110
|
+
...k
|
|
111
111
|
}
|
|
112
112
|
)
|
|
113
113
|
] });
|
|
114
|
-
const
|
|
115
|
-
return /* @__PURE__ */
|
|
116
|
-
/* @__PURE__ */
|
|
117
|
-
/* @__PURE__ */
|
|
114
|
+
const p = O(e, z, "webp"), g = O(e, z);
|
|
115
|
+
return /* @__PURE__ */ q("picture", { "data-routerino-image": "true", "data-original-src": e, children: [
|
|
116
|
+
/* @__PURE__ */ m("source", { srcSet: p, type: "image/webp", sizes: l }),
|
|
117
|
+
/* @__PURE__ */ m(
|
|
118
118
|
"img",
|
|
119
119
|
{
|
|
120
120
|
src: e,
|
|
121
|
-
alt:
|
|
122
|
-
srcSet:
|
|
123
|
-
sizes:
|
|
124
|
-
loading:
|
|
125
|
-
decoding:
|
|
121
|
+
alt: c,
|
|
122
|
+
srcSet: g,
|
|
123
|
+
sizes: l,
|
|
124
|
+
loading: a,
|
|
125
|
+
decoding: j,
|
|
126
126
|
fetchPriority: P,
|
|
127
|
-
className:
|
|
128
|
-
style:
|
|
129
|
-
...
|
|
130
|
-
...
|
|
127
|
+
className: u,
|
|
128
|
+
style: o,
|
|
129
|
+
...b,
|
|
130
|
+
...k
|
|
131
131
|
}
|
|
132
132
|
)
|
|
133
133
|
] });
|
|
134
134
|
}
|
|
135
135
|
oe.propTypes = {
|
|
136
136
|
/** Image source URL (required) */
|
|
137
|
-
src:
|
|
137
|
+
src: t.string.isRequired,
|
|
138
138
|
/** Alt text for accessibility (required) */
|
|
139
|
-
alt:
|
|
139
|
+
alt: t.string.isRequired,
|
|
140
140
|
/** Override lazy loading for hero/LCP images */
|
|
141
|
-
priority:
|
|
141
|
+
priority: t.bool,
|
|
142
142
|
/** Responsive widths to generate (defaults to [480, 800, 1200, 1920]) */
|
|
143
|
-
widths:
|
|
143
|
+
widths: t.arrayOf(t.number),
|
|
144
144
|
/** Responsive sizes attribute (has smart default) */
|
|
145
|
-
sizes:
|
|
145
|
+
sizes: t.string,
|
|
146
146
|
/** CSS classes (Tailwind/DaisyUI ready) */
|
|
147
|
-
className:
|
|
147
|
+
className: t.string,
|
|
148
148
|
/** Inline styles */
|
|
149
|
-
style:
|
|
149
|
+
style: t.object,
|
|
150
150
|
/** Explicit width for CLS prevention */
|
|
151
|
-
width:
|
|
151
|
+
width: t.number,
|
|
152
152
|
/** Explicit height for CLS prevention */
|
|
153
|
-
height:
|
|
153
|
+
height: t.number,
|
|
154
154
|
/** Loading behavior (auto-set based on priority) */
|
|
155
|
-
loading:
|
|
155
|
+
loading: t.oneOf(["lazy", "eager"]),
|
|
156
156
|
/** Decode timing */
|
|
157
|
-
decoding:
|
|
157
|
+
decoding: t.oneOf(["sync", "async", "auto"]),
|
|
158
158
|
/** Fetch priority (auto-set based on priority) */
|
|
159
|
-
fetchpriority:
|
|
159
|
+
fetchpriority: t.oneOf(["high", "low", "auto"])
|
|
160
160
|
};
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
const ne = [
|
|
162
|
+
".pdf",
|
|
163
|
+
".doc",
|
|
164
|
+
".docx",
|
|
165
|
+
".xls",
|
|
166
|
+
".xlsx",
|
|
167
|
+
".ppt",
|
|
168
|
+
".pptx",
|
|
169
|
+
".odt",
|
|
170
|
+
".ods",
|
|
171
|
+
".odp",
|
|
172
|
+
".rtf",
|
|
173
|
+
".csv",
|
|
174
|
+
".txt",
|
|
175
|
+
".md",
|
|
176
|
+
".png",
|
|
177
|
+
".jpg",
|
|
178
|
+
".jpeg",
|
|
179
|
+
".gif",
|
|
180
|
+
".svg",
|
|
181
|
+
".webp",
|
|
182
|
+
".avif",
|
|
183
|
+
".ico",
|
|
184
|
+
".bmp",
|
|
185
|
+
".tiff",
|
|
186
|
+
".tif",
|
|
187
|
+
".mp4",
|
|
188
|
+
".webm",
|
|
189
|
+
".ogv",
|
|
190
|
+
".mov",
|
|
191
|
+
".avi",
|
|
192
|
+
".mkv",
|
|
193
|
+
".flv",
|
|
194
|
+
".mp3",
|
|
195
|
+
".wav",
|
|
196
|
+
".ogg",
|
|
197
|
+
".flac",
|
|
198
|
+
".aac",
|
|
199
|
+
".m4a",
|
|
200
|
+
".wma",
|
|
201
|
+
".woff",
|
|
202
|
+
".woff2",
|
|
203
|
+
".ttf",
|
|
204
|
+
".eot",
|
|
205
|
+
".zip",
|
|
206
|
+
".tar",
|
|
207
|
+
".gz",
|
|
208
|
+
".bz2",
|
|
209
|
+
".7z",
|
|
210
|
+
".rar",
|
|
211
|
+
".xz",
|
|
212
|
+
".zst",
|
|
213
|
+
".epub",
|
|
214
|
+
".mobi",
|
|
215
|
+
".json",
|
|
216
|
+
".xml",
|
|
217
|
+
".yml",
|
|
218
|
+
".yaml",
|
|
219
|
+
".css",
|
|
220
|
+
".js",
|
|
221
|
+
".map",
|
|
222
|
+
".wasm"
|
|
223
|
+
], N = Z(null);
|
|
224
|
+
function ue() {
|
|
225
|
+
const i = Q(N);
|
|
164
226
|
if (!i)
|
|
165
227
|
throw new Error(
|
|
166
228
|
"useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component."
|
|
167
229
|
);
|
|
168
230
|
return i;
|
|
169
231
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
232
|
+
function h({
|
|
233
|
+
tag: i = "meta",
|
|
234
|
+
soft: e = !1,
|
|
235
|
+
innerHTML: c,
|
|
236
|
+
...n
|
|
237
|
+
}) {
|
|
238
|
+
const f = Object.keys(n);
|
|
239
|
+
if (f.length < 1)
|
|
173
240
|
return console.error(
|
|
174
241
|
`[Routerino] updateHeadTag() received no attributes to set for ${i} tag`
|
|
175
242
|
);
|
|
176
|
-
let
|
|
177
|
-
for (let
|
|
178
|
-
`${i}[${
|
|
179
|
-
)), !
|
|
243
|
+
let l = null;
|
|
244
|
+
for (let u = 0; u < f.length && (f[u] !== "content" && (l = document.querySelector(
|
|
245
|
+
`${i}[${f[u]}='${n[f[u]]}']`
|
|
246
|
+
)), !l); u++)
|
|
180
247
|
;
|
|
181
|
-
|
|
248
|
+
l && e || (l || (l = document.createElement(i)), f.forEach((u) => l.setAttribute(u, n[u])), c !== void 0 && (l.innerHTML = c), document.querySelector("head").appendChild(l));
|
|
182
249
|
}
|
|
183
|
-
function
|
|
184
|
-
let
|
|
185
|
-
return n.forEach((
|
|
186
|
-
|
|
187
|
-
}),
|
|
250
|
+
function re({ routePattern: i, currentRoute: e }) {
|
|
251
|
+
let c = {}, n = i.split("/"), f = e.split("/");
|
|
252
|
+
return n.forEach((l, u) => {
|
|
253
|
+
l.startsWith(":") && (c[l.slice(1)] = f[u]);
|
|
254
|
+
}), c;
|
|
188
255
|
}
|
|
189
|
-
class
|
|
256
|
+
class G extends J {
|
|
190
257
|
constructor(e) {
|
|
191
258
|
super(e), this.state = { hasError: !1 };
|
|
192
259
|
}
|
|
193
260
|
static getDerivedStateFromError() {
|
|
194
261
|
return { hasError: !0 };
|
|
195
262
|
}
|
|
196
|
-
componentDidCatch(e,
|
|
263
|
+
componentDidCatch(e, c) {
|
|
197
264
|
this.props.debug && (console.group(
|
|
198
265
|
"%c[Routerino]%c Error Boundary Caught an Error",
|
|
199
266
|
"color: #ff6b6b; font-weight: bold",
|
|
200
267
|
"",
|
|
201
268
|
e
|
|
202
|
-
), console.error("[Routerino] Component Stack:",
|
|
269
|
+
), console.error("[Routerino] Component Stack:", c.componentStack), this.props.routePath && console.error("[Routerino] Failed Route:", this.props.routePath), console.error("[Routerino] Error occurred at:", (/* @__PURE__ */ new Date()).toISOString()), console.groupEnd()), document.title = this.props.errorTitleString, this.props.usePrerenderTags && h({ name: "prerender-status-code", content: "500" });
|
|
203
270
|
}
|
|
204
271
|
render() {
|
|
205
272
|
return this.state.hasError ? this.props.fallback : this.props.children;
|
|
206
273
|
}
|
|
207
274
|
}
|
|
208
|
-
|
|
275
|
+
G.propTypes = {
|
|
209
276
|
/** The child components to render when there's no error */
|
|
210
|
-
children:
|
|
277
|
+
children: t.node,
|
|
211
278
|
/** The fallback UI to display when an error is caught */
|
|
212
|
-
fallback:
|
|
279
|
+
fallback: t.node,
|
|
213
280
|
/** The document title to set when an error occurs */
|
|
214
|
-
errorTitleString:
|
|
281
|
+
errorTitleString: t.string.isRequired,
|
|
215
282
|
/** Whether to set prerender meta tags (status code 500) on error */
|
|
216
|
-
usePrerenderTags:
|
|
283
|
+
usePrerenderTags: t.bool,
|
|
217
284
|
/** The current route path for better error context (optional) */
|
|
218
|
-
routePath:
|
|
285
|
+
routePath: t.string,
|
|
219
286
|
/** Whether to log debug messages to console (optional) */
|
|
220
|
-
debug:
|
|
287
|
+
debug: t.bool
|
|
221
288
|
};
|
|
222
|
-
function
|
|
289
|
+
function ie({
|
|
223
290
|
routes: i = [
|
|
224
291
|
{
|
|
225
292
|
path: "/",
|
|
226
|
-
element: /* @__PURE__ */
|
|
293
|
+
element: /* @__PURE__ */ m("p", { children: "This is the default route. Pass an array of routes to the Routerino component in order to configure your own pages. Each route is a dictionary with at least `path` and `element` defined." }),
|
|
227
294
|
title: "Routerino default route example",
|
|
228
295
|
description: "The default route example description.",
|
|
229
296
|
tags: [{ property: "og:locale", content: "en_US" }]
|
|
230
297
|
}
|
|
231
298
|
],
|
|
232
|
-
notFoundTemplate: e = /* @__PURE__ */
|
|
233
|
-
/* @__PURE__ */
|
|
234
|
-
/* @__PURE__ */
|
|
299
|
+
notFoundTemplate: e = /* @__PURE__ */ q(V, { children: [
|
|
300
|
+
/* @__PURE__ */ m("p", { children: "No page found for this URL. [404]" }),
|
|
301
|
+
/* @__PURE__ */ m("p", { children: /* @__PURE__ */ m("a", { href: "/", children: "Home" }) })
|
|
235
302
|
] }),
|
|
236
|
-
notFoundTitle:
|
|
237
|
-
errorTemplate: n = /* @__PURE__ */
|
|
238
|
-
/* @__PURE__ */
|
|
239
|
-
/* @__PURE__ */
|
|
303
|
+
notFoundTitle: c = "Page not found [404]",
|
|
304
|
+
errorTemplate: n = /* @__PURE__ */ q(V, { children: [
|
|
305
|
+
/* @__PURE__ */ m("p", { children: "Page failed to load. [500]" }),
|
|
306
|
+
/* @__PURE__ */ m("p", { children: /* @__PURE__ */ m("a", { href: "/", children: "Home" }) })
|
|
240
307
|
] }),
|
|
241
|
-
errorTitle:
|
|
242
|
-
useTrailingSlash:
|
|
243
|
-
usePrerenderTags:
|
|
244
|
-
baseUrl:
|
|
245
|
-
title:
|
|
246
|
-
separator:
|
|
247
|
-
imageUrl:
|
|
248
|
-
touchIconUrl:
|
|
249
|
-
debug:
|
|
308
|
+
errorTitle: f = "Page error [500]",
|
|
309
|
+
useTrailingSlash: l = !0,
|
|
310
|
+
usePrerenderTags: u = !1,
|
|
311
|
+
baseUrl: F = null,
|
|
312
|
+
title: v = "",
|
|
313
|
+
separator: $ = " | ",
|
|
314
|
+
imageUrl: A = null,
|
|
315
|
+
touchIconUrl: j = null,
|
|
316
|
+
debug: d = !1,
|
|
317
|
+
ignorePatterns: k = []
|
|
250
318
|
}) {
|
|
251
|
-
const
|
|
319
|
+
const U = [f, v].filter(Boolean).join($), S = [c, v].filter(Boolean).join($);
|
|
252
320
|
try {
|
|
253
|
-
if (
|
|
254
|
-
const
|
|
255
|
-
(
|
|
321
|
+
if (d) {
|
|
322
|
+
const o = i.map((g) => g.path), p = o.filter(
|
|
323
|
+
(g, r) => o.indexOf(g) !== r
|
|
256
324
|
);
|
|
257
|
-
|
|
325
|
+
p.length > 0 && (console.warn(
|
|
258
326
|
"%c[Routerino]%c Duplicate route paths detected:",
|
|
259
327
|
"color: #f59e0b; font-weight: bold",
|
|
260
328
|
"",
|
|
261
|
-
[...new Set(
|
|
329
|
+
[...new Set(p)]
|
|
262
330
|
), console.warn(
|
|
263
331
|
"%c[Routerino]%c The first matching route will be used",
|
|
264
332
|
"color: #f59e0b; font-weight: bold",
|
|
265
333
|
""
|
|
266
334
|
));
|
|
267
335
|
}
|
|
268
|
-
const [
|
|
336
|
+
const [x, W] = M(window?.location?.href ?? "/");
|
|
269
337
|
_(() => {
|
|
270
338
|
if (typeof window > "u" || typeof document > "u")
|
|
271
339
|
return;
|
|
272
|
-
const
|
|
273
|
-
|
|
340
|
+
const o = (g) => {
|
|
341
|
+
d && console.debug(
|
|
274
342
|
"%c[Routerino]%c click occurred",
|
|
275
343
|
"color: #6b7280; font-weight: bold",
|
|
276
344
|
""
|
|
277
345
|
);
|
|
278
|
-
let
|
|
279
|
-
for (;
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
346
|
+
let r = g.target;
|
|
347
|
+
for (; r.tagName !== "A" && r.parentElement; )
|
|
348
|
+
r = r.parentElement;
|
|
349
|
+
if (r.tagName !== "A") {
|
|
350
|
+
d && console.debug(
|
|
283
351
|
"%c[Routerino]%c no anchor tag found during click",
|
|
284
352
|
"color: #6b7280; font-weight: bold",
|
|
285
353
|
""
|
|
286
354
|
);
|
|
287
355
|
return;
|
|
288
356
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
357
|
+
if (g.defaultPrevented || g.button !== 0 || g.ctrlKey || g.metaKey || g.shiftKey || g.altKey || r.getAttribute("target") === "_blank" || r.hasAttribute("download") || r.getAttribute("rel") === "external")
|
|
358
|
+
return;
|
|
359
|
+
const w = r.getAttribute("href") || r.href;
|
|
360
|
+
if (!w) {
|
|
361
|
+
d && console.debug(
|
|
292
362
|
"%c[Routerino]%c anchor tag has no href",
|
|
293
363
|
"color: #6b7280; font-weight: bold",
|
|
294
364
|
""
|
|
295
365
|
);
|
|
296
366
|
return;
|
|
297
367
|
}
|
|
298
|
-
if (!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(
|
|
299
|
-
|
|
368
|
+
if (!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(w)) {
|
|
369
|
+
d && console.debug(
|
|
300
370
|
"%c[Routerino]%c skipping non-http URL:",
|
|
301
371
|
"color: #6b7280; font-weight: bold",
|
|
302
372
|
"",
|
|
303
|
-
|
|
373
|
+
w
|
|
304
374
|
);
|
|
305
375
|
return;
|
|
306
376
|
}
|
|
307
|
-
|
|
377
|
+
d && console.debug(
|
|
308
378
|
"%c[Routerino]%c click target href:",
|
|
309
379
|
"color: #6b7280; font-weight: bold",
|
|
310
380
|
"",
|
|
311
|
-
|
|
381
|
+
w
|
|
312
382
|
);
|
|
313
|
-
let
|
|
383
|
+
let y;
|
|
314
384
|
try {
|
|
315
|
-
|
|
316
|
-
} catch (
|
|
317
|
-
|
|
385
|
+
y = new URL(w, window.location.href);
|
|
386
|
+
} catch (T) {
|
|
387
|
+
d && console.debug(
|
|
318
388
|
"%c[Routerino]%c Invalid URL:",
|
|
319
389
|
"color: #6b7280; font-weight: bold",
|
|
320
390
|
"",
|
|
321
|
-
|
|
322
|
-
|
|
391
|
+
w,
|
|
392
|
+
T
|
|
323
393
|
);
|
|
324
394
|
return;
|
|
325
395
|
}
|
|
326
|
-
if (
|
|
396
|
+
if (d && console.debug(
|
|
327
397
|
"%c[Routerino]%c targetUrl:",
|
|
328
398
|
"color: #6b7280; font-weight: bold",
|
|
329
399
|
"",
|
|
330
|
-
|
|
400
|
+
y,
|
|
331
401
|
"current:",
|
|
332
402
|
window.location
|
|
333
|
-
),
|
|
334
|
-
|
|
403
|
+
), y && window.location.origin === y.origin) {
|
|
404
|
+
const T = y.pathname.toLowerCase();
|
|
405
|
+
if (ne.some((R) => T.endsWith(R))) {
|
|
406
|
+
d && console.debug(
|
|
407
|
+
"%c[Routerino]%c skipping file extension link:",
|
|
408
|
+
"color: #6b7280; font-weight: bold",
|
|
409
|
+
"",
|
|
410
|
+
w
|
|
411
|
+
);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (k.length > 0 && k.some(
|
|
415
|
+
(R) => new RegExp(R, "i").test(w)
|
|
416
|
+
)) {
|
|
417
|
+
d && console.debug(
|
|
418
|
+
"%c[Routerino]%c skipping ignored link pattern:",
|
|
419
|
+
"color: #6b7280; font-weight: bold",
|
|
420
|
+
"",
|
|
421
|
+
w
|
|
422
|
+
);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (d && console.debug(
|
|
335
426
|
"%c[Routerino]%c target link is same origin, will use push-state transitioning",
|
|
336
427
|
"color: #6b7280; font-weight: bold",
|
|
337
428
|
""
|
|
338
|
-
),
|
|
339
|
-
const
|
|
429
|
+
), g.preventDefault(), r.href !== window.location.href && (W(r.href), window.history.pushState({}, "", r.href)), r.hash) {
|
|
430
|
+
const R = decodeURIComponent(r.hash.slice(1));
|
|
340
431
|
setTimeout(() => {
|
|
341
|
-
const
|
|
342
|
-
|
|
432
|
+
const L = document.getElementById(R);
|
|
433
|
+
L ? L.scrollIntoView({ behavior: "auto" }) : window.scrollTo({ top: 0, behavior: "auto" });
|
|
343
434
|
}, 0);
|
|
344
435
|
} else
|
|
345
436
|
window.scrollTo({
|
|
346
437
|
top: 0,
|
|
347
438
|
behavior: "auto"
|
|
348
439
|
});
|
|
349
|
-
else
|
|
440
|
+
} else d && console.debug(
|
|
350
441
|
"%c[Routerino]%c target link does not share an origin, standard browser link handling applies",
|
|
351
442
|
"color: #6b7280; font-weight: bold",
|
|
352
443
|
""
|
|
353
444
|
);
|
|
354
445
|
};
|
|
355
|
-
document.addEventListener("click",
|
|
356
|
-
const
|
|
357
|
-
|
|
446
|
+
document.addEventListener("click", o);
|
|
447
|
+
const p = () => {
|
|
448
|
+
d && console.debug(
|
|
358
449
|
"%c[Routerino]%c route change ->",
|
|
359
450
|
"color: #6b7280; font-weight: bold",
|
|
360
451
|
"",
|
|
361
452
|
window.location.pathname
|
|
362
|
-
),
|
|
453
|
+
), W(window.location.href);
|
|
363
454
|
};
|
|
364
|
-
return window.addEventListener("popstate",
|
|
365
|
-
document.removeEventListener("click",
|
|
455
|
+
return window.addEventListener("popstate", p), () => {
|
|
456
|
+
document.removeEventListener("click", o), window.removeEventListener("popstate", p);
|
|
366
457
|
};
|
|
367
|
-
}, [
|
|
368
|
-
let
|
|
369
|
-
(
|
|
370
|
-
const
|
|
371
|
-
(
|
|
372
|
-
),
|
|
373
|
-
const
|
|
374
|
-
return
|
|
375
|
-
}), s =
|
|
376
|
-
if (
|
|
458
|
+
}, [x, k]);
|
|
459
|
+
let a = window?.location?.pathname ?? "/";
|
|
460
|
+
(a === "/index.html" || a === "") && (a = "/");
|
|
461
|
+
const P = i.find((o) => o.path === a), E = i.find(
|
|
462
|
+
(o) => `${o.path}/` === a || o.path === `${a}/`
|
|
463
|
+
), C = i.find((o) => {
|
|
464
|
+
const p = o.path.endsWith("/") ? o.path.slice(0, -1) : o.path, g = a.endsWith("/") ? a.slice(0, -1) : a, r = p.split("/").filter(Boolean), w = g.split("/").filter(Boolean);
|
|
465
|
+
return r.length !== w.length ? !1 : r.every((y, T) => y.startsWith(":") ? !0 : y === w[T]);
|
|
466
|
+
}), s = P ?? E ?? C;
|
|
467
|
+
if (d && console.debug(
|
|
377
468
|
"%c[Routerino]%c Route matching:",
|
|
378
469
|
"color: #6b7280; font-weight: bold",
|
|
379
470
|
"",
|
|
380
|
-
{ match: s, exactMatch:
|
|
471
|
+
{ match: s, exactMatch: P, addSlashMatch: E, paramsMatch: C }
|
|
381
472
|
), !s)
|
|
382
|
-
return
|
|
473
|
+
return d && (console.group(
|
|
383
474
|
"%c[Routerino]%c 404 - No matching route",
|
|
384
475
|
"color: #f59e0b; font-weight: bold",
|
|
385
476
|
""
|
|
@@ -387,84 +478,84 @@ function re({
|
|
|
387
478
|
"%c[Routerino]%c Requested path:",
|
|
388
479
|
"color: #f59e0b; font-weight: bold",
|
|
389
480
|
"",
|
|
390
|
-
|
|
481
|
+
a
|
|
391
482
|
), console.warn(
|
|
392
483
|
"%c[Routerino]%c Available routes:",
|
|
393
484
|
"color: #f59e0b; font-weight: bold",
|
|
394
485
|
"",
|
|
395
|
-
i.map((
|
|
396
|
-
), console.groupEnd()), document.title =
|
|
397
|
-
if (
|
|
398
|
-
const
|
|
486
|
+
i.map((o) => o.path)
|
|
487
|
+
), console.groupEnd()), document.title = S, u && h({ name: "prerender-status-code", content: "404" }), e;
|
|
488
|
+
if (u) {
|
|
489
|
+
const o = document.querySelector(
|
|
399
490
|
'meta[name="prerender-status-code"]'
|
|
400
491
|
);
|
|
401
|
-
|
|
402
|
-
const
|
|
492
|
+
o && o.remove();
|
|
493
|
+
const p = document.querySelector(
|
|
403
494
|
'meta[name="prerender-header"]'
|
|
404
495
|
);
|
|
405
|
-
|
|
496
|
+
p && p.remove();
|
|
406
497
|
}
|
|
407
|
-
const
|
|
408
|
-
if (s.title) {
|
|
409
|
-
const
|
|
410
|
-
document.title =
|
|
411
|
-
tag: "link",
|
|
412
|
-
rel: "canonical",
|
|
413
|
-
href: U
|
|
414
|
-
}), s.tags?.find(({ property: l }) => l === "og:title") || p({
|
|
498
|
+
const B = l && !a.endsWith("/") && a !== "/", D = !l && a.endsWith("/") && a !== "/", z = B ? `${a}/` : D ? a.slice(0, -1) : a, b = `${F ?? window?.location?.origin ?? ""}${z}`;
|
|
499
|
+
if (s.title || v) {
|
|
500
|
+
const o = [s.title, v].filter(Boolean).join($);
|
|
501
|
+
document.title = o, s.tags?.find(({ property: p }) => p === "og:title") || h({
|
|
415
502
|
property: "og:title",
|
|
416
|
-
content:
|
|
417
|
-
}), s.tags?.find(({ property: l }) => l === "og:url") || p({
|
|
418
|
-
property: "og:url",
|
|
419
|
-
content: U
|
|
503
|
+
content: o
|
|
420
504
|
});
|
|
421
505
|
}
|
|
422
|
-
if (
|
|
506
|
+
if (h({
|
|
507
|
+
tag: "link",
|
|
508
|
+
rel: "canonical",
|
|
509
|
+
href: b
|
|
510
|
+
}), s.tags?.find(({ property: o }) => o === "og:url") || h({
|
|
511
|
+
property: "og:url",
|
|
512
|
+
content: b
|
|
513
|
+
}), s.description && (h({ name: "description", content: s.description }), s.tags?.find(({ property: o }) => o === "og:description") || h({
|
|
423
514
|
property: "og:description",
|
|
424
515
|
content: s.description
|
|
425
|
-
})), (
|
|
516
|
+
})), (A || s.imageUrl) && h({
|
|
426
517
|
property: "og:image",
|
|
427
|
-
content: s.imageUrl ??
|
|
428
|
-
}), s.tags?.find(({ property:
|
|
518
|
+
content: s.imageUrl ?? A
|
|
519
|
+
}), s.tags?.find(({ property: o }) => o === "twitter:card") || h({
|
|
429
520
|
name: "twitter:card",
|
|
430
521
|
content: "summary_large_image"
|
|
431
|
-
}),
|
|
522
|
+
}), j && h({
|
|
432
523
|
tag: "link",
|
|
433
524
|
rel: "apple-touch-icon",
|
|
434
|
-
href:
|
|
435
|
-
}),
|
|
525
|
+
href: j
|
|
526
|
+
}), u && (B || D) && (h({ name: "prerender-status-code", content: "301" }), h({
|
|
436
527
|
name: "prerender-header",
|
|
437
|
-
content: `Location: ${
|
|
438
|
-
})), s.tags && s.tags.length ? (s.tags.find(({ property:
|
|
439
|
-
const
|
|
528
|
+
content: `Location: ${b}`
|
|
529
|
+
})), s.tags && s.tags.length ? (s.tags.find(({ property: o }) => o === "og:type") || h({ property: "og:type", content: "website" }), s.tags.forEach((o) => h(o))) : h({ property: "og:type", content: "website" }), s.element) {
|
|
530
|
+
const o = re({
|
|
440
531
|
routePattern: s.path,
|
|
441
|
-
currentRoute:
|
|
442
|
-
}),
|
|
443
|
-
currentRoute:
|
|
444
|
-
params:
|
|
532
|
+
currentRoute: a
|
|
533
|
+
}), p = {
|
|
534
|
+
currentRoute: a,
|
|
535
|
+
params: o,
|
|
445
536
|
routePattern: s.path,
|
|
446
|
-
updateHeadTag:
|
|
537
|
+
updateHeadTag: h
|
|
447
538
|
};
|
|
448
|
-
return /* @__PURE__ */
|
|
449
|
-
|
|
539
|
+
return /* @__PURE__ */ m(N.Provider, { value: p, children: /* @__PURE__ */ m(
|
|
540
|
+
G,
|
|
450
541
|
{
|
|
451
542
|
fallback: n,
|
|
452
|
-
errorTitleString:
|
|
453
|
-
usePrerenderTags:
|
|
454
|
-
routePath:
|
|
455
|
-
debug:
|
|
543
|
+
errorTitleString: U,
|
|
544
|
+
usePrerenderTags: u,
|
|
545
|
+
routePath: a,
|
|
546
|
+
debug: d,
|
|
456
547
|
children: s.element
|
|
457
548
|
}
|
|
458
549
|
) });
|
|
459
550
|
}
|
|
460
|
-
return
|
|
551
|
+
return d && console.error(
|
|
461
552
|
"%c[Routerino]%c No route found for",
|
|
462
553
|
"color: #ff6b6b; font-weight: bold",
|
|
463
554
|
"",
|
|
464
|
-
|
|
465
|
-
), document.title =
|
|
466
|
-
} catch (
|
|
467
|
-
return
|
|
555
|
+
a
|
|
556
|
+
), document.title = S, u && h({ name: "prerender-status-code", content: "404" }), e;
|
|
557
|
+
} catch (x) {
|
|
558
|
+
return d && (console.group(
|
|
468
559
|
"%c[Routerino]%c Fatal Error",
|
|
469
560
|
"color: #ff6b6b; font-weight: bold",
|
|
470
561
|
""
|
|
@@ -476,64 +567,65 @@ function re({
|
|
|
476
567
|
"%c[Routerino]%c Error:",
|
|
477
568
|
"color: #ff6b6b; font-weight: bold",
|
|
478
569
|
"",
|
|
479
|
-
|
|
570
|
+
x
|
|
480
571
|
), console.error(
|
|
481
572
|
"%c[Routerino]%c This typically means an issue with route configuration or router setup",
|
|
482
573
|
"color: #ff6b6b; font-weight: bold",
|
|
483
574
|
""
|
|
484
|
-
), console.groupEnd()),
|
|
575
|
+
), console.groupEnd()), u && h({ name: "prerender-status-code", content: "500" }), document.title = U, n;
|
|
485
576
|
}
|
|
486
577
|
}
|
|
487
|
-
const
|
|
488
|
-
path: (i, e,
|
|
578
|
+
const ce = t.exact({
|
|
579
|
+
path: (i, e, c) => {
|
|
489
580
|
const n = i[e];
|
|
490
581
|
return n == null ? new Error(
|
|
491
|
-
`The prop \`${e}\` is marked as required in \`${
|
|
582
|
+
`The prop \`${e}\` is marked as required in \`${c}\`, but its value is \`${n}\`.`
|
|
492
583
|
) : typeof n != "string" ? new Error(
|
|
493
|
-
`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${
|
|
584
|
+
`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${c}\`, expected \`string\`.`
|
|
494
585
|
) : n.startsWith("/") ? null : new Error(
|
|
495
|
-
`Invalid prop \`${e}\` value \`${n}\` supplied to \`${
|
|
586
|
+
`Invalid prop \`${e}\` value \`${n}\` supplied to \`${c}\`. Route paths must start with a forward slash (/).`
|
|
496
587
|
);
|
|
497
588
|
},
|
|
498
|
-
element:
|
|
499
|
-
title:
|
|
500
|
-
description:
|
|
501
|
-
tags:
|
|
502
|
-
imageUrl:
|
|
589
|
+
element: t.element.isRequired,
|
|
590
|
+
title: t.string,
|
|
591
|
+
description: t.string,
|
|
592
|
+
tags: t.arrayOf(t.object),
|
|
593
|
+
imageUrl: t.string
|
|
503
594
|
});
|
|
504
|
-
|
|
505
|
-
routes:
|
|
506
|
-
title:
|
|
507
|
-
separator:
|
|
508
|
-
notFoundTemplate:
|
|
509
|
-
notFoundTitle:
|
|
510
|
-
errorTemplate:
|
|
511
|
-
errorTitle:
|
|
512
|
-
useTrailingSlash:
|
|
513
|
-
usePrerenderTags:
|
|
514
|
-
baseUrl: (i, e,
|
|
595
|
+
ie.propTypes = {
|
|
596
|
+
routes: t.arrayOf(ce),
|
|
597
|
+
title: t.string,
|
|
598
|
+
separator: t.string,
|
|
599
|
+
notFoundTemplate: t.element,
|
|
600
|
+
notFoundTitle: t.string,
|
|
601
|
+
errorTemplate: t.element,
|
|
602
|
+
errorTitle: t.string,
|
|
603
|
+
useTrailingSlash: t.bool,
|
|
604
|
+
usePrerenderTags: t.bool,
|
|
605
|
+
baseUrl: (i, e, c) => {
|
|
515
606
|
const n = i[e];
|
|
516
607
|
if (n != null) {
|
|
517
608
|
if (typeof n != "string")
|
|
518
609
|
return new Error(
|
|
519
|
-
`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${
|
|
610
|
+
`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${c}\`, expected \`string\`.`
|
|
520
611
|
);
|
|
521
612
|
if (n.endsWith("/"))
|
|
522
613
|
return new Error(
|
|
523
|
-
`Invalid prop \`${e}\` supplied to \`${
|
|
614
|
+
`Invalid prop \`${e}\` supplied to \`${c}\`. The baseUrl should not end with a slash. Got: "${n}"`
|
|
524
615
|
);
|
|
525
616
|
}
|
|
526
617
|
return null;
|
|
527
618
|
},
|
|
528
|
-
imageUrl:
|
|
529
|
-
touchIconUrl:
|
|
530
|
-
debug:
|
|
619
|
+
imageUrl: t.string,
|
|
620
|
+
touchIconUrl: t.string,
|
|
621
|
+
debug: t.bool,
|
|
622
|
+
ignorePatterns: t.arrayOf(t.string)
|
|
531
623
|
};
|
|
532
624
|
export {
|
|
533
|
-
|
|
625
|
+
G as ErrorBoundary,
|
|
534
626
|
oe as Image,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
627
|
+
ie as Routerino,
|
|
628
|
+
ie as default,
|
|
629
|
+
h as updateHeadTag,
|
|
630
|
+
ue as useRouterino
|
|
539
631
|
};
|
package/dist/routerino.umd.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(w,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],c):(w=typeof globalThis<"u"?globalThis:w||self,c(w.routerino={},w["react/jsx-runtime"],w.React,w.PropTypes))})(this,(function(w,c,b,o){"use strict";const Q=[480,800,1200,1920],X="(max-width: 480px) 100vw, (max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px",A=new Map;function Y(i,e=""){const r=i.toLowerCase(),n=e.toLowerCase();return!!(r.includes("hero")||r.includes("banner")||n.includes("hero")||n.includes("banner")||n.includes("h-screen")||n.includes("h-full"))}function D(i,e,r=null){const n=i.replace(/\.(jpe?g|png|webp)$/i,""),f=r?`.${r}`:i.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg";return e.map(s=>`${n}-${s}w${f} ${s}w`).join(", ")}function Z(i){const{src:e="",alt:r="",priority:n,widths:f=Q,sizes:s=X,className:m="",style:V={},width:R,height:x,loading:B,decoding:C="async",fetchpriority:p,...L}=i||{},q=typeof window>"u",S=!q&&typeof document<"u"&&typeof HTMLElement<"u"&&document.createElement("div")instanceof HTMLElement,y=S&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1"),a=n??Y(e,m),W=B||(a?"eager":"lazy"),I=p||(a?"high":void 0),[E,l]=b.useState(null);b.useEffect(()=>{if(!S||y||!e)return;const g=new window.Image;g.onload=()=>{l({width:g.naturalWidth,height:g.naturalHeight})},g.src=e},[e,S,y]);const k=b.useMemo(()=>E?f.filter(g=>E.width>=g):f,[E,f]),[O,G]=b.useState(null);b.useEffect(()=>{if(!S||y)return;let g=!1;return(async()=>{const U=e.replace(/\.(jpe?g|png|webp)$/i,""),F=e.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg",M=[];for(const K of k){const H=`${U}-${K}w${F}`;if(A.has(H)){A.get(H)&&M.push(K);continue}try{const N=await fetch(H,{method:"HEAD"});A.set(H,N.ok),N.ok&&M.push(K)}catch{A.set(H,!1)}}g||G(M.length>0?M:k)})(),()=>{g=!0}},[e,k,S,y]);const j=O??k,t={};R!=null&&(t.width=R),x!=null&&(t.height=x),E&&R==null&&x==null&&(t.width=E.width,t.height=E.height);const u={maxWidth:"100%",height:"auto",...V};if(y)return c.jsx("img",{src:e,alt:r,loading:W,decoding:C,fetchPriority:I,className:m,style:u,...t,...L});if(q)return c.jsxs("picture",{"data-routerino-image":"true","data-original-src":e,children:[c.jsx("source",{srcSet:D(e,f,"webp"),type:"image/webp",sizes:s}),c.jsx("img",{src:e,alt:r,srcSet:D(e,f),sizes:s,loading:W,decoding:"async",fetchPriority:I,className:m,style:u,...t,...L})]});const $=D(e,j,"webp"),h=D(e,j);return c.jsxs("picture",{"data-routerino-image":"true","data-original-src":e,children:[c.jsx("source",{srcSet:$,type:"image/webp",sizes:s}),c.jsx("img",{src:e,alt:r,srcSet:h,sizes:s,loading:W,decoding:C,fetchPriority:I,className:m,style:u,...t,...L})]})}Z.propTypes={src:o.string.isRequired,alt:o.string.isRequired,priority:o.bool,widths:o.arrayOf(o.number),sizes:o.string,className:o.string,style:o.object,width:o.number,height:o.number,loading:o.oneOf(["lazy","eager"]),decoding:o.oneOf(["sync","async","auto"]),fetchpriority:o.oneOf(["high","low","auto"])};const J=b.createContext(null);function T(){const i=b.useContext(J);if(!i)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return i}function d({tag:i="meta",soft:e=!1,...r}){const n=Object.keys(r);if(n.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${i} tag`);let f=null;for(let s=0;s<n.length&&(n[s]!=="content"&&(f=document.querySelector(`${i}[${n[s]}='${r[n[s]]}']`)),!f);s++);f&&e||(f||(f=document.createElement(i)),n.forEach(s=>f.setAttribute(s,r[s])),document.querySelector("head").appendChild(f))}function P({routePattern:i,currentRoute:e}){let r={},n=i.split("/"),f=e.split("/");return n.forEach((s,m)=>{s.startsWith(":")&&(r[s.slice(1)]=f[m])}),r}class _ extends b.Component{constructor(e){super(e),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e,r){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",e),console.error("[Routerino] Component Stack:",r.componentStack),this.props.routePath&&console.error("[Routerino] Failed Route:",this.props.routePath),console.error("[Routerino] Error occurred at:",new Date().toISOString()),console.groupEnd()),document.title=this.props.errorTitleString,this.props.usePrerenderTags&&d({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}_.propTypes={children:o.node,fallback:o.node,errorTitleString:o.string.isRequired,usePrerenderTags:o.bool,routePath:o.string,debug:o.bool};function z({routes:i=[{path:"/",element:c.jsx("p",{children:"This is the default route. Pass an array of routes to the Routerino component in order to configure your own pages. Each route is a dictionary with at least `path` and `element` defined."}),title:"Routerino default route example",description:"The default route example description.",tags:[{property:"og:locale",content:"en_US"}]}],notFoundTemplate:e=c.jsxs(c.Fragment,{children:[c.jsx("p",{children:"No page found for this URL. [404]"}),c.jsx("p",{children:c.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:r="Page not found [404]",errorTemplate:n=c.jsxs(c.Fragment,{children:[c.jsx("p",{children:"Page failed to load. [500]"}),c.jsx("p",{children:c.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:f="Page error [500]",useTrailingSlash:s=!0,usePrerenderTags:m=!1,baseUrl:V=null,title:R="",separator:x=" | ",imageUrl:B=null,touchIconUrl:C=null,debug:p=!1}){const L=`${f}${x}${R}`,q=`${r}${x}${R}`;try{if(p){const t=i.map($=>$.path),u=t.filter(($,h)=>t.indexOf($)!==h);u.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(u)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[S,y]=b.useState(window?.location?.href??"/");b.useEffect(()=>{if(typeof window>"u"||typeof document>"u")return;const t=$=>{p&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let h=$.target;for(;h.tagName!=="A"&&h.parentElement;)h=h.parentElement;if(h.tagName!=="A"){p&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}const g=h.getAttribute("href")||h.href;if(!g){p&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(g)){p&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",g);return}p&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",g);let v;try{v=new URL(g,window.location.href)}catch(U){p&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",g,U);return}if(p&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",v,"current:",window.location),v&&window.location.origin===v.origin)if(p&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),$.preventDefault(),h.href!==window.location.href&&(y(h.href),window.history.pushState({},"",h.href)),h.hash){const U=decodeURIComponent(h.hash.slice(1));setTimeout(()=>{const F=document.getElementById(U);F?F.scrollIntoView({behavior:"auto"}):window.scrollTo({top:0,behavior:"auto"})},0)}else window.scrollTo({top:0,behavior:"auto"});else p&&console.debug("%c[Routerino]%c target link does not share an origin, standard browser link handling applies","color: #6b7280; font-weight: bold","")};document.addEventListener("click",t);const u=()=>{p&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),y(window.location.href)};return window.addEventListener("popstate",u),()=>{document.removeEventListener("click",t),window.removeEventListener("popstate",u)}},[S]);let a=window?.location?.pathname??"/";(a==="/index.html"||a==="")&&(a="/");const W=i.find(t=>t.path===a),I=i.find(t=>`${t.path}/`===a||t.path===`${a}/`),E=i.find(t=>{const u=t.path.endsWith("/")?t.path.slice(0,-1):t.path,$=a.endsWith("/")?a.slice(0,-1):a,h=u.split("/").filter(Boolean),g=$.split("/").filter(Boolean);return h.length!==g.length?!1:h.every((v,U)=>v.startsWith(":")?!0:v===g[U])}),l=W??I??E;if(p&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:l,exactMatch:W,addSlashMatch:I,paramsMatch:E}),!l)return p&&(console.group("%c[Routerino]%c 404 - No matching route","color: #f59e0b; font-weight: bold",""),console.warn("%c[Routerino]%c Requested path:","color: #f59e0b; font-weight: bold","",a),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",i.map(t=>t.path)),console.groupEnd()),document.title=q,m&&d({name:"prerender-status-code",content:"404"}),e;if(m){const t=document.querySelector('meta[name="prerender-status-code"]');t&&t.remove();const u=document.querySelector('meta[name="prerender-header"]');u&&u.remove()}const k=s&&!a.endsWith("/")&&a!=="/",O=!s&&a.endsWith("/")&&a!=="/",G=k?`${a}/`:O?a.slice(0,-1):a,j=`${V??window?.location?.origin??""}${G}`;if(l.title){const t=`${l.title}${x}${R}`;document.title=t,d({tag:"link",rel:"canonical",href:j}),l.tags?.find(({property:u})=>u==="og:title")||d({property:"og:title",content:t}),l.tags?.find(({property:u})=>u==="og:url")||d({property:"og:url",content:j})}if(l.description&&(d({name:"description",content:l.description}),l.tags?.find(({property:t})=>t==="og:description")||d({property:"og:description",content:l.description})),(B||l.imageUrl)&&d({property:"og:image",content:l.imageUrl??B}),l.tags?.find(({property:t})=>t==="twitter:card")||d({name:"twitter:card",content:"summary_large_image"}),C&&d({tag:"link",rel:"apple-touch-icon",href:C}),m&&(k||O)&&(d({name:"prerender-status-code",content:"301"}),d({name:"prerender-header",content:`Location: ${j}`})),l.tags&&l.tags.length?(l.tags.find(({property:t})=>t==="og:type")||d({property:"og:type",content:"website"}),l.tags.forEach(t=>d(t))):d({property:"og:type",content:"website"}),l.element){const t=P({routePattern:l.path,currentRoute:a}),u={currentRoute:a,params:t,routePattern:l.path,updateHeadTag:d};return c.jsx(J.Provider,{value:u,children:c.jsx(_,{fallback:n,errorTitleString:L,usePrerenderTags:m,routePath:a,debug:p,children:l.element})})}return p&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",a),document.title=q,m&&d({name:"prerender-status-code",content:"404"}),e}catch(S){return p&&(console.group("%c[Routerino]%c Fatal Error","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c An error occurred in the router itself (not in a route component)","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c Error:","color: #ff6b6b; font-weight: bold","",S),console.error("%c[Routerino]%c This typically means an issue with route configuration or router setup","color: #ff6b6b; font-weight: bold",""),console.groupEnd()),m&&d({name:"prerender-status-code",content:"500"}),document.title=L,n}}const ee=o.exact({path:(i,e,r)=>{const n=i[e];return n==null?new Error(`The prop \`${e}\` is marked as required in \`${r}\`, but its value is \`${n}\`.`):typeof n!="string"?new Error(`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${r}\`, expected \`string\`.`):n.startsWith("/")?null:new Error(`Invalid prop \`${e}\` value \`${n}\` supplied to \`${r}\`. Route paths must start with a forward slash (/).`)},element:o.element.isRequired,title:o.string,description:o.string,tags:o.arrayOf(o.object),imageUrl:o.string});z.propTypes={routes:o.arrayOf(ee),title:o.string,separator:o.string,notFoundTemplate:o.element,notFoundTitle:o.string,errorTemplate:o.element,errorTitle:o.string,useTrailingSlash:o.bool,usePrerenderTags:o.bool,baseUrl:(i,e,r)=>{const n=i[e];if(n!=null){if(typeof n!="string")return new Error(`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${r}\`, expected \`string\`.`);if(n.endsWith("/"))return new Error(`Invalid prop \`${e}\` supplied to \`${r}\`. The baseUrl should not end with a slash. Got: "${n}"`)}return null},imageUrl:o.string,touchIconUrl:o.string,debug:o.bool},w.ErrorBoundary=_,w.Image=Z,w.Routerino=z,w.default=z,w.updateHeadTag=d,w.useRouterino=T,Object.defineProperties(w,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
|
1
|
+
(function(m,s){typeof exports=="object"&&typeof module<"u"?s(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],s):(m=typeof globalThis<"u"?globalThis:m||self,s(m.routerino={},m["react/jsx-runtime"],m.React,m.PropTypes))})(this,(function(m,s,y,e){"use strict";const J=[480,800,1200,1920],Q="(max-width: 480px) 100vw, (max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px",D=new Map;function Y(i,t=""){const c=i.toLowerCase(),n=t.toLowerCase();return!!(c.includes("hero")||c.includes("banner")||n.includes("hero")||n.includes("banner")||n.includes("h-screen")||n.includes("h-full"))}function H(i,t,c=null){const n=i.replace(/\.(jpe?g|png|webp)$/i,""),g=c?`.${c}`:i.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg";return t.map(u=>`${n}-${u}w${g} ${u}w`).join(", ")}function G(i){const{src:t="",alt:c="",priority:n,widths:g=J,sizes:u=Q,className:d="",style:N={},width:$,height:j,loading:O,decoding:A="async",fetchpriority:f,...L}=i||{},C=typeof window>"u",k=!C&&typeof document<"u"&&typeof HTMLElement<"u"&&document.createElement("div")instanceof HTMLElement,x=k&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1"),B=n??Y(t,d),l=O||(B?"eager":"lazy"),W=f||(B?"high":void 0),[R,z]=y.useState(null);y.useEffect(()=>{if(!k||x||!t)return;const r=new window.Image;r.onload=()=>{z({width:r.naturalWidth,height:r.naturalHeight})},r.src=t},[t,k,x]);const a=y.useMemo(()=>R?g.filter(r=>R.width>=r):g,[R,g]),[F,M]=y.useState(null);y.useEffect(()=>{if(!k||x)return;let r=!1;return(async()=>{const S=t.replace(/\.(jpe?g|png|webp)$/i,""),U=t.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg",v=[];for(const I of a){const q=`${S}-${I}w${U}`;if(D.has(q)){D.get(q)&&v.push(I);continue}try{const Z=await fetch(q,{method:"HEAD"});D.set(q,Z.ok),Z.ok&&v.push(I)}catch{D.set(q,!1)}}r||M(v.length>0?v:a)})(),()=>{r=!0}},[t,a,k,x]);const _=F??a,E={};$!=null&&(E.width=$),j!=null&&(E.height=j),R&&$==null&&j==null&&(E.width=R.width,E.height=R.height);const o={maxWidth:"100%",height:"auto",...N};if(x)return s.jsx("img",{src:t,alt:c,loading:l,decoding:A,fetchPriority:W,className:d,style:o,...E,...L});if(C)return s.jsxs("picture",{"data-routerino-image":"true","data-original-src":t,children:[s.jsx("source",{srcSet:H(t,g,"webp"),type:"image/webp",sizes:u}),s.jsx("img",{src:t,alt:c,srcSet:H(t,g),sizes:u,loading:l,decoding:"async",fetchPriority:W,className:d,style:o,...E,...L})]});const p=H(t,_,"webp"),w=H(t,_);return s.jsxs("picture",{"data-routerino-image":"true","data-original-src":t,children:[s.jsx("source",{srcSet:p,type:"image/webp",sizes:u}),s.jsx("img",{src:t,alt:c,srcSet:w,sizes:u,loading:l,decoding:A,fetchPriority:W,className:d,style:o,...E,...L})]})}G.propTypes={src:e.string.isRequired,alt:e.string.isRequired,priority:e.bool,widths:e.arrayOf(e.number),sizes:e.string,className:e.string,style:e.object,width:e.number,height:e.number,loading:e.oneOf(["lazy","eager"]),decoding:e.oneOf(["sync","async","auto"]),fetchpriority:e.oneOf(["high","low","auto"])};const T=[".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".odt",".ods",".odp",".rtf",".csv",".txt",".md",".png",".jpg",".jpeg",".gif",".svg",".webp",".avif",".ico",".bmp",".tiff",".tif",".mp4",".webm",".ogv",".mov",".avi",".mkv",".flv",".mp3",".wav",".ogg",".flac",".aac",".m4a",".wma",".woff",".woff2",".ttf",".eot",".zip",".tar",".gz",".bz2",".7z",".rar",".xz",".zst",".epub",".mobi",".json",".xml",".yml",".yaml",".css",".js",".map",".wasm"],X=y.createContext(null);function P(){const i=y.useContext(X);if(!i)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return i}function h({tag:i="meta",soft:t=!1,innerHTML:c,...n}){const g=Object.keys(n);if(g.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${i} tag`);let u=null;for(let d=0;d<g.length&&(g[d]!=="content"&&(u=document.querySelector(`${i}[${g[d]}='${n[g[d]]}']`)),!u);d++);u&&t||(u||(u=document.createElement(i)),g.forEach(d=>u.setAttribute(d,n[d])),c!==void 0&&(u.innerHTML=c),document.querySelector("head").appendChild(u))}function ee({routePattern:i,currentRoute:t}){let c={},n=i.split("/"),g=t.split("/");return n.forEach((u,d)=>{u.startsWith(":")&&(c[u.slice(1)]=g[d])}),c}class K extends y.Component{constructor(t){super(t),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(t,c){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",t),console.error("[Routerino] Component Stack:",c.componentStack),this.props.routePath&&console.error("[Routerino] Failed Route:",this.props.routePath),console.error("[Routerino] Error occurred at:",new Date().toISOString()),console.groupEnd()),document.title=this.props.errorTitleString,this.props.usePrerenderTags&&h({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}K.propTypes={children:e.node,fallback:e.node,errorTitleString:e.string.isRequired,usePrerenderTags:e.bool,routePath:e.string,debug:e.bool};function V({routes:i=[{path:"/",element:s.jsx("p",{children:"This is the default route. Pass an array of routes to the Routerino component in order to configure your own pages. Each route is a dictionary with at least `path` and `element` defined."}),title:"Routerino default route example",description:"The default route example description.",tags:[{property:"og:locale",content:"en_US"}]}],notFoundTemplate:t=s.jsxs(s.Fragment,{children:[s.jsx("p",{children:"No page found for this URL. [404]"}),s.jsx("p",{children:s.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:c="Page not found [404]",errorTemplate:n=s.jsxs(s.Fragment,{children:[s.jsx("p",{children:"Page failed to load. [500]"}),s.jsx("p",{children:s.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:g="Page error [500]",useTrailingSlash:u=!0,usePrerenderTags:d=!1,baseUrl:N=null,title:$="",separator:j=" | ",imageUrl:O=null,touchIconUrl:A=null,debug:f=!1,ignorePatterns:L=[]}){const C=[g,$].filter(Boolean).join(j),k=[c,$].filter(Boolean).join(j);try{if(f){const o=i.map(w=>w.path),p=o.filter((w,r)=>o.indexOf(w)!==r);p.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(p)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[x,B]=y.useState(window?.location?.href??"/");y.useEffect(()=>{if(typeof window>"u"||typeof document>"u")return;const o=w=>{f&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let r=w.target;for(;r.tagName!=="A"&&r.parentElement;)r=r.parentElement;if(r.tagName!=="A"){f&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}if(w.defaultPrevented||w.button!==0||w.ctrlKey||w.metaKey||w.shiftKey||w.altKey||r.getAttribute("target")==="_blank"||r.hasAttribute("download")||r.getAttribute("rel")==="external")return;const b=r.getAttribute("href")||r.href;if(!b){f&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(b)){f&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",b);return}f&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",b);let S;try{S=new URL(b,window.location.href)}catch(U){f&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",b,U);return}if(f&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",S,"current:",window.location),S&&window.location.origin===S.origin){const U=S.pathname.toLowerCase();if(T.some(v=>U.endsWith(v))){f&&console.debug("%c[Routerino]%c skipping file extension link:","color: #6b7280; font-weight: bold","",b);return}if(L.length>0&&L.some(v=>new RegExp(v,"i").test(b))){f&&console.debug("%c[Routerino]%c skipping ignored link pattern:","color: #6b7280; font-weight: bold","",b);return}if(f&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),w.preventDefault(),r.href!==window.location.href&&(B(r.href),window.history.pushState({},"",r.href)),r.hash){const v=decodeURIComponent(r.hash.slice(1));setTimeout(()=>{const I=document.getElementById(v);I?I.scrollIntoView({behavior:"auto"}):window.scrollTo({top:0,behavior:"auto"})},0)}else window.scrollTo({top:0,behavior:"auto"})}else f&&console.debug("%c[Routerino]%c target link does not share an origin, standard browser link handling applies","color: #6b7280; font-weight: bold","")};document.addEventListener("click",o);const p=()=>{f&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),B(window.location.href)};return window.addEventListener("popstate",p),()=>{document.removeEventListener("click",o),window.removeEventListener("popstate",p)}},[x,L]);let l=window?.location?.pathname??"/";(l==="/index.html"||l==="")&&(l="/");const W=i.find(o=>o.path===l),R=i.find(o=>`${o.path}/`===l||o.path===`${l}/`),z=i.find(o=>{const p=o.path.endsWith("/")?o.path.slice(0,-1):o.path,w=l.endsWith("/")?l.slice(0,-1):l,r=p.split("/").filter(Boolean),b=w.split("/").filter(Boolean);return r.length!==b.length?!1:r.every((S,U)=>S.startsWith(":")?!0:S===b[U])}),a=W??R??z;if(f&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:a,exactMatch:W,addSlashMatch:R,paramsMatch:z}),!a)return f&&(console.group("%c[Routerino]%c 404 - No matching route","color: #f59e0b; font-weight: bold",""),console.warn("%c[Routerino]%c Requested path:","color: #f59e0b; font-weight: bold","",l),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",i.map(o=>o.path)),console.groupEnd()),document.title=k,d&&h({name:"prerender-status-code",content:"404"}),t;if(d){const o=document.querySelector('meta[name="prerender-status-code"]');o&&o.remove();const p=document.querySelector('meta[name="prerender-header"]');p&&p.remove()}const F=u&&!l.endsWith("/")&&l!=="/",M=!u&&l.endsWith("/")&&l!=="/",_=F?`${l}/`:M?l.slice(0,-1):l,E=`${N??window?.location?.origin??""}${_}`;if(a.title||$){const o=[a.title,$].filter(Boolean).join(j);document.title=o,a.tags?.find(({property:p})=>p==="og:title")||h({property:"og:title",content:o})}if(h({tag:"link",rel:"canonical",href:E}),a.tags?.find(({property:o})=>o==="og:url")||h({property:"og:url",content:E}),a.description&&(h({name:"description",content:a.description}),a.tags?.find(({property:o})=>o==="og:description")||h({property:"og:description",content:a.description})),(O||a.imageUrl)&&h({property:"og:image",content:a.imageUrl??O}),a.tags?.find(({property:o})=>o==="twitter:card")||h({name:"twitter:card",content:"summary_large_image"}),A&&h({tag:"link",rel:"apple-touch-icon",href:A}),d&&(F||M)&&(h({name:"prerender-status-code",content:"301"}),h({name:"prerender-header",content:`Location: ${E}`})),a.tags&&a.tags.length?(a.tags.find(({property:o})=>o==="og:type")||h({property:"og:type",content:"website"}),a.tags.forEach(o=>h(o))):h({property:"og:type",content:"website"}),a.element){const o=ee({routePattern:a.path,currentRoute:l}),p={currentRoute:l,params:o,routePattern:a.path,updateHeadTag:h};return s.jsx(X.Provider,{value:p,children:s.jsx(K,{fallback:n,errorTitleString:C,usePrerenderTags:d,routePath:l,debug:f,children:a.element})})}return f&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",l),document.title=k,d&&h({name:"prerender-status-code",content:"404"}),t}catch(x){return f&&(console.group("%c[Routerino]%c Fatal Error","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c An error occurred in the router itself (not in a route component)","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c Error:","color: #ff6b6b; font-weight: bold","",x),console.error("%c[Routerino]%c This typically means an issue with route configuration or router setup","color: #ff6b6b; font-weight: bold",""),console.groupEnd()),d&&h({name:"prerender-status-code",content:"500"}),document.title=C,n}}const te=e.exact({path:(i,t,c)=>{const n=i[t];return n==null?new Error(`The prop \`${t}\` is marked as required in \`${c}\`, but its value is \`${n}\`.`):typeof n!="string"?new Error(`Invalid prop \`${t}\` of type \`${typeof n}\` supplied to \`${c}\`, expected \`string\`.`):n.startsWith("/")?null:new Error(`Invalid prop \`${t}\` value \`${n}\` supplied to \`${c}\`. Route paths must start with a forward slash (/).`)},element:e.element.isRequired,title:e.string,description:e.string,tags:e.arrayOf(e.object),imageUrl:e.string});V.propTypes={routes:e.arrayOf(te),title:e.string,separator:e.string,notFoundTemplate:e.element,notFoundTitle:e.string,errorTemplate:e.element,errorTitle:e.string,useTrailingSlash:e.bool,usePrerenderTags:e.bool,baseUrl:(i,t,c)=>{const n=i[t];if(n!=null){if(typeof n!="string")return new Error(`Invalid prop \`${t}\` of type \`${typeof n}\` supplied to \`${c}\`, expected \`string\`.`);if(n.endsWith("/"))return new Error(`Invalid prop \`${t}\` supplied to \`${c}\`. The baseUrl should not end with a slash. Got: "${n}"`)}return null},imageUrl:e.string,touchIconUrl:e.string,debug:e.bool,ignorePatterns:e.arrayOf(e.string)},m.ErrorBoundary=K,m.Image=G,m.Routerino=V,m.default=V,m.updateHeadTag=h,m.useRouterino=P,Object.defineProperties(m,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
package/package.json
CHANGED
package/routerino-forge.js
CHANGED
|
@@ -1229,13 +1229,20 @@ function generateMetaTags(route, config, urlPath) {
|
|
|
1229
1229
|
if (route.tags && Array.isArray(route.tags)) {
|
|
1230
1230
|
route.tags.forEach((tag) => {
|
|
1231
1231
|
const tagName = tag.tag || "meta";
|
|
1232
|
+
const innerHTML = tag.innerHTML;
|
|
1232
1233
|
const attrs = Object.entries(tag)
|
|
1233
|
-
.filter(
|
|
1234
|
+
.filter(
|
|
1235
|
+
([key]) => key !== "tag" && key !== "soft" && key !== "innerHTML"
|
|
1236
|
+
)
|
|
1234
1237
|
.map(([key, value]) => formatMetaAttribute(key, value))
|
|
1235
1238
|
.join(" ");
|
|
1236
1239
|
|
|
1237
|
-
if (attrs) {
|
|
1238
|
-
|
|
1240
|
+
if (attrs || innerHTML !== undefined) {
|
|
1241
|
+
if (innerHTML !== undefined) {
|
|
1242
|
+
tags.push(`<${tagName} ${attrs}>${innerHTML}</${tagName}>`);
|
|
1243
|
+
} else {
|
|
1244
|
+
tags.push(`<${tagName} ${attrs}>`);
|
|
1245
|
+
}
|
|
1239
1246
|
}
|
|
1240
1247
|
});
|
|
1241
1248
|
}
|
package/types/routerino.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export interface ImageProps
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface HeadTag {
|
|
19
|
+
/** The HTML tag name to update (default: "meta") */
|
|
19
20
|
tag?: string;
|
|
21
|
+
/** Whether to skip the update of an existing tag if already exists (default: false) */
|
|
20
22
|
soft?: boolean;
|
|
21
23
|
name?: string;
|
|
22
24
|
property?: string;
|
|
@@ -32,6 +34,8 @@ export interface HeadTag {
|
|
|
32
34
|
media?: string;
|
|
33
35
|
hrefLang?: string;
|
|
34
36
|
target?: string;
|
|
37
|
+
/** Inner HTML content for tags that require body content (e.g., <script>, <style>) */
|
|
38
|
+
innerHTML?: string;
|
|
35
39
|
[attribute: string]: string | number | boolean | object | undefined;
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -64,6 +68,14 @@ export interface RouterinoProps {
|
|
|
64
68
|
imageUrl?: string;
|
|
65
69
|
touchIconUrl?: string;
|
|
66
70
|
debug?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Array of regex pattern strings to match against link hrefs.
|
|
73
|
+
* Matching links will NOT be intercepted by the SPA router and
|
|
74
|
+
* will be handled natively by the browser instead.
|
|
75
|
+
* Patterns are case-insensitive.
|
|
76
|
+
* @example ignorePatterns={["/api/", "/admin/legacy/"]}
|
|
77
|
+
*/
|
|
78
|
+
ignorePatterns?: string[];
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
export interface ErrorBoundaryProps {
|