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 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). Routerino handles same-origin anchor clicks. Cross-origin links and non-HTTP schemes (e.g., mailto:, tel:) are handled by the browser as usual.
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`. This is useful when you want to specify the production URL regardless of the current environment.
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 after React renders the new page, it finds the element with the matching `id` and scrolls it into view.
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 g, jsxs as B, Fragment as G } from "react/jsx-runtime";
2
- import { useState as z, useEffect as _, useMemo as J, createContext as N, Component as Q, useContext as X } from "react";
3
- import o from "prop-types";
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 r = i.toLowerCase(), n = e.toLowerCase();
7
- return !!(r.includes("hero") || r.includes("banner") || n.includes("hero") || n.includes("banner") || n.includes("h-screen") || n.includes("h-full"));
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 q(i, e, r = null) {
10
- const n = i.replace(/\.(jpe?g|png|webp)$/i, ""), u = r ? `.${r}` : i.match(/\.(jpe?g|png|webp)$/i)?.[0] || ".jpg";
11
- return e.map((a) => `${n}-${a}w${u} ${a}w`).join(", ");
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: r = "",
16
+ alt: c = "",
17
17
  priority: n,
18
- widths: u = Y,
19
- sizes: a = ee,
20
- className: m = "",
21
- style: O = {},
22
- width: E,
23
- height: S,
24
- loading: I,
25
- decoding: L = "async",
26
- fetchpriority: f,
27
- ...T
28
- } = i || {}, W = typeof window > "u", b = !W && typeof document < "u" && typeof HTMLElement < "u" && document.createElement("div") instanceof HTMLElement, R = b && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"), c = n ?? te(e, m), k = I || (c ? "eager" : "lazy"), P = f || (c ? "high" : void 0), [y, s] = z(null);
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 (!b || R || !e) return;
31
- const h = new window.Image();
32
- h.onload = () => {
33
- s({
34
- width: h.naturalWidth,
35
- height: h.naturalHeight
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
- }, h.src = e;
38
- }, [e, b, R]);
39
- const v = J(() => y ? u.filter((h) => y.width >= h) : u, [y, u]), [j, F] = z(null);
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 (!b || R) return;
42
- let h = !1;
41
+ if (!S || x) return;
42
+ let r = !1;
43
43
  return (async () => {
44
- const x = e.replace(/\.(jpe?g|png|webp)$/i, ""), A = e.match(/\.(jpe?g|png|webp)$/i)?.[0] || ".jpg", D = [];
45
- for (const M of v) {
46
- const C = `${x}-${M}w${A}`;
47
- if (H.has(C)) {
48
- H.get(C) && D.push(M);
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 V = await fetch(C, { method: "HEAD" });
53
- H.set(C, V.ok), V.ok && D.push(M);
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(C, !1);
55
+ H.set(I, !1);
56
56
  }
57
57
  }
58
- h || F(
59
- D.length > 0 ? D : v
58
+ r || D(
59
+ R.length > 0 ? R : s
60
60
  );
61
61
  })(), () => {
62
- h = !0;
62
+ r = !0;
63
63
  };
64
- }, [e, v, b, R]);
65
- const U = j ?? v, t = {};
66
- E != null && (t.width = E), S != null && (t.height = S), y && E == null && S == null && (t.width = y.width, t.height = y.height);
67
- const l = {
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
- ...O
70
+ ...F
71
71
  };
72
- if (R)
73
- return /* @__PURE__ */ g(
72
+ if (x)
73
+ return /* @__PURE__ */ m(
74
74
  "img",
75
75
  {
76
76
  src: e,
77
- alt: r,
78
- loading: k,
79
- decoding: L,
77
+ alt: c,
78
+ loading: a,
79
+ decoding: j,
80
80
  fetchPriority: P,
81
- className: m,
82
- style: l,
83
- ...t,
84
- ...T
81
+ className: u,
82
+ style: o,
83
+ ...b,
84
+ ...k
85
85
  }
86
86
  );
87
- if (W)
88
- return /* @__PURE__ */ B("picture", { "data-routerino-image": "true", "data-original-src": e, children: [
89
- /* @__PURE__ */ g(
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: q(e, u, "webp"),
92
+ srcSet: O(e, f, "webp"),
93
93
  type: "image/webp",
94
- sizes: a
94
+ sizes: l
95
95
  }
96
96
  ),
97
- /* @__PURE__ */ g(
97
+ /* @__PURE__ */ m(
98
98
  "img",
99
99
  {
100
100
  src: e,
101
- alt: r,
102
- srcSet: q(e, u),
103
- sizes: a,
104
- loading: k,
101
+ alt: c,
102
+ srcSet: O(e, f),
103
+ sizes: l,
104
+ loading: a,
105
105
  decoding: "async",
106
106
  fetchPriority: P,
107
- className: m,
108
- style: l,
109
- ...t,
110
- ...T
107
+ className: u,
108
+ style: o,
109
+ ...b,
110
+ ...k
111
111
  }
112
112
  )
113
113
  ] });
114
- const w = q(e, U, "webp"), d = q(e, U);
115
- return /* @__PURE__ */ B("picture", { "data-routerino-image": "true", "data-original-src": e, children: [
116
- /* @__PURE__ */ g("source", { srcSet: w, type: "image/webp", sizes: a }),
117
- /* @__PURE__ */ g(
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: r,
122
- srcSet: d,
123
- sizes: a,
124
- loading: k,
125
- decoding: L,
121
+ alt: c,
122
+ srcSet: g,
123
+ sizes: l,
124
+ loading: a,
125
+ decoding: j,
126
126
  fetchPriority: P,
127
- className: m,
128
- style: l,
129
- ...t,
130
- ...T
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: o.string.isRequired,
137
+ src: t.string.isRequired,
138
138
  /** Alt text for accessibility (required) */
139
- alt: o.string.isRequired,
139
+ alt: t.string.isRequired,
140
140
  /** Override lazy loading for hero/LCP images */
141
- priority: o.bool,
141
+ priority: t.bool,
142
142
  /** Responsive widths to generate (defaults to [480, 800, 1200, 1920]) */
143
- widths: o.arrayOf(o.number),
143
+ widths: t.arrayOf(t.number),
144
144
  /** Responsive sizes attribute (has smart default) */
145
- sizes: o.string,
145
+ sizes: t.string,
146
146
  /** CSS classes (Tailwind/DaisyUI ready) */
147
- className: o.string,
147
+ className: t.string,
148
148
  /** Inline styles */
149
- style: o.object,
149
+ style: t.object,
150
150
  /** Explicit width for CLS prevention */
151
- width: o.number,
151
+ width: t.number,
152
152
  /** Explicit height for CLS prevention */
153
- height: o.number,
153
+ height: t.number,
154
154
  /** Loading behavior (auto-set based on priority) */
155
- loading: o.oneOf(["lazy", "eager"]),
155
+ loading: t.oneOf(["lazy", "eager"]),
156
156
  /** Decode timing */
157
- decoding: o.oneOf(["sync", "async", "auto"]),
157
+ decoding: t.oneOf(["sync", "async", "auto"]),
158
158
  /** Fetch priority (auto-set based on priority) */
159
- fetchpriority: o.oneOf(["high", "low", "auto"])
159
+ fetchpriority: t.oneOf(["high", "low", "auto"])
160
160
  };
161
- const K = N(null);
162
- function le() {
163
- const i = X(K);
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 p({ tag: i = "meta", soft: e = !1, ...r }) {
171
- const n = Object.keys(r);
172
- if (n.length < 1)
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 u = null;
177
- for (let a = 0; a < n.length && (n[a] !== "content" && (u = document.querySelector(
178
- `${i}[${n[a]}='${r[n[a]]}']`
179
- )), !u); a++)
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
- u && e || (u || (u = document.createElement(i)), n.forEach((a) => u.setAttribute(a, r[a])), document.querySelector("head").appendChild(u));
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 ne({ routePattern: i, currentRoute: e }) {
184
- let r = {}, n = i.split("/"), u = e.split("/");
185
- return n.forEach((a, m) => {
186
- a.startsWith(":") && (r[a.slice(1)] = u[m]);
187
- }), r;
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 Z extends Q {
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, r) {
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:", r.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 && p({ name: "prerender-status-code", content: "500" });
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
- Z.propTypes = {
275
+ G.propTypes = {
209
276
  /** The child components to render when there's no error */
210
- children: o.node,
277
+ children: t.node,
211
278
  /** The fallback UI to display when an error is caught */
212
- fallback: o.node,
279
+ fallback: t.node,
213
280
  /** The document title to set when an error occurs */
214
- errorTitleString: o.string.isRequired,
281
+ errorTitleString: t.string.isRequired,
215
282
  /** Whether to set prerender meta tags (status code 500) on error */
216
- usePrerenderTags: o.bool,
283
+ usePrerenderTags: t.bool,
217
284
  /** The current route path for better error context (optional) */
218
- routePath: o.string,
285
+ routePath: t.string,
219
286
  /** Whether to log debug messages to console (optional) */
220
- debug: o.bool
287
+ debug: t.bool
221
288
  };
222
- function re({
289
+ function ie({
223
290
  routes: i = [
224
291
  {
225
292
  path: "/",
226
- element: /* @__PURE__ */ g("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." }),
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__ */ B(G, { children: [
233
- /* @__PURE__ */ g("p", { children: "No page found for this URL. [404]" }),
234
- /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
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: r = "Page not found [404]",
237
- errorTemplate: n = /* @__PURE__ */ B(G, { children: [
238
- /* @__PURE__ */ g("p", { children: "Page failed to load. [500]" }),
239
- /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
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: u = "Page error [500]",
242
- useTrailingSlash: a = !0,
243
- usePrerenderTags: m = !1,
244
- baseUrl: O = null,
245
- title: E = "",
246
- separator: S = " | ",
247
- imageUrl: I = null,
248
- touchIconUrl: L = null,
249
- debug: f = !1
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 T = `${u}${S}${E}`, W = `${r}${S}${E}`;
319
+ const U = [f, v].filter(Boolean).join($), S = [c, v].filter(Boolean).join($);
252
320
  try {
253
- if (f) {
254
- const t = i.map((w) => w.path), l = t.filter(
255
- (w, d) => t.indexOf(w) !== d
321
+ if (d) {
322
+ const o = i.map((g) => g.path), p = o.filter(
323
+ (g, r) => o.indexOf(g) !== r
256
324
  );
257
- l.length > 0 && (console.warn(
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(l)]
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 [b, R] = z(window?.location?.href ?? "/");
336
+ const [x, W] = M(window?.location?.href ?? "/");
269
337
  _(() => {
270
338
  if (typeof window > "u" || typeof document > "u")
271
339
  return;
272
- const t = (w) => {
273
- f && console.debug(
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 d = w.target;
279
- for (; d.tagName !== "A" && d.parentElement; )
280
- d = d.parentElement;
281
- if (d.tagName !== "A") {
282
- f && console.debug(
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
- const h = d.getAttribute("href") || d.href;
290
- if (!h) {
291
- f && console.debug(
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(h)) {
299
- f && console.debug(
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
- h
373
+ w
304
374
  );
305
375
  return;
306
376
  }
307
- f && console.debug(
377
+ d && console.debug(
308
378
  "%c[Routerino]%c click target href:",
309
379
  "color: #6b7280; font-weight: bold",
310
380
  "",
311
- h
381
+ w
312
382
  );
313
- let $;
383
+ let y;
314
384
  try {
315
- $ = new URL(h, window.location.href);
316
- } catch (x) {
317
- f && console.debug(
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
- h,
322
- x
391
+ w,
392
+ T
323
393
  );
324
394
  return;
325
395
  }
326
- if (f && console.debug(
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
- ), $ && window.location.origin === $.origin)
334
- if (f && console.debug(
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
- ), w.preventDefault(), d.href !== window.location.href && (R(d.href), window.history.pushState({}, "", d.href)), d.hash) {
339
- const x = decodeURIComponent(d.hash.slice(1));
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 A = document.getElementById(x);
342
- A ? A.scrollIntoView({ behavior: "auto" }) : window.scrollTo({ top: 0, behavior: "auto" });
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 f && console.debug(
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", t);
356
- const l = () => {
357
- f && console.debug(
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
- ), R(window.location.href);
453
+ ), W(window.location.href);
363
454
  };
364
- return window.addEventListener("popstate", l), () => {
365
- document.removeEventListener("click", t), window.removeEventListener("popstate", l);
455
+ return window.addEventListener("popstate", p), () => {
456
+ document.removeEventListener("click", o), window.removeEventListener("popstate", p);
366
457
  };
367
- }, [b]);
368
- let c = window?.location?.pathname ?? "/";
369
- (c === "/index.html" || c === "") && (c = "/");
370
- const k = i.find((t) => t.path === c), P = i.find(
371
- (t) => `${t.path}/` === c || t.path === `${c}/`
372
- ), y = i.find((t) => {
373
- const l = t.path.endsWith("/") ? t.path.slice(0, -1) : t.path, w = c.endsWith("/") ? c.slice(0, -1) : c, d = l.split("/").filter(Boolean), h = w.split("/").filter(Boolean);
374
- return d.length !== h.length ? !1 : d.every(($, x) => $.startsWith(":") ? !0 : $ === h[x]);
375
- }), s = k ?? P ?? y;
376
- if (f && console.debug(
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: k, addSlashMatch: P, paramsMatch: y }
471
+ { match: s, exactMatch: P, addSlashMatch: E, paramsMatch: C }
381
472
  ), !s)
382
- return f && (console.group(
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
- c
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((t) => t.path)
396
- ), console.groupEnd()), document.title = W, m && p({ name: "prerender-status-code", content: "404" }), e;
397
- if (m) {
398
- const t = document.querySelector(
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
- t && t.remove();
402
- const l = document.querySelector(
492
+ o && o.remove();
493
+ const p = document.querySelector(
403
494
  'meta[name="prerender-header"]'
404
495
  );
405
- l && l.remove();
496
+ p && p.remove();
406
497
  }
407
- const v = a && !c.endsWith("/") && c !== "/", j = !a && c.endsWith("/") && c !== "/", F = v ? `${c}/` : j ? c.slice(0, -1) : c, U = `${O ?? window?.location?.origin ?? ""}${F}`;
408
- if (s.title) {
409
- const t = `${s.title}${S}${E}`;
410
- document.title = t, p({
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: t
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 (s.description && (p({ name: "description", content: s.description }), s.tags?.find(({ property: t }) => t === "og:description") || p({
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
- })), (I || s.imageUrl) && p({
516
+ })), (A || s.imageUrl) && h({
426
517
  property: "og:image",
427
- content: s.imageUrl ?? I
428
- }), s.tags?.find(({ property: t }) => t === "twitter:card") || p({
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
- }), L && p({
522
+ }), j && h({
432
523
  tag: "link",
433
524
  rel: "apple-touch-icon",
434
- href: L
435
- }), m && (v || j) && (p({ name: "prerender-status-code", content: "301" }), p({
525
+ href: j
526
+ }), u && (B || D) && (h({ name: "prerender-status-code", content: "301" }), h({
436
527
  name: "prerender-header",
437
- content: `Location: ${U}`
438
- })), s.tags && s.tags.length ? (s.tags.find(({ property: t }) => t === "og:type") || p({ property: "og:type", content: "website" }), s.tags.forEach((t) => p(t))) : p({ property: "og:type", content: "website" }), s.element) {
439
- const t = ne({
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: c
442
- }), l = {
443
- currentRoute: c,
444
- params: t,
532
+ currentRoute: a
533
+ }), p = {
534
+ currentRoute: a,
535
+ params: o,
445
536
  routePattern: s.path,
446
- updateHeadTag: p
537
+ updateHeadTag: h
447
538
  };
448
- return /* @__PURE__ */ g(K.Provider, { value: l, children: /* @__PURE__ */ g(
449
- Z,
539
+ return /* @__PURE__ */ m(N.Provider, { value: p, children: /* @__PURE__ */ m(
540
+ G,
450
541
  {
451
542
  fallback: n,
452
- errorTitleString: T,
453
- usePrerenderTags: m,
454
- routePath: c,
455
- debug: f,
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 f && console.error(
551
+ return d && console.error(
461
552
  "%c[Routerino]%c No route found for",
462
553
  "color: #ff6b6b; font-weight: bold",
463
554
  "",
464
- c
465
- ), document.title = W, m && p({ name: "prerender-status-code", content: "404" }), e;
466
- } catch (b) {
467
- return f && (console.group(
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
- b
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()), m && p({ name: "prerender-status-code", content: "500" }), document.title = T, n;
575
+ ), console.groupEnd()), u && h({ name: "prerender-status-code", content: "500" }), document.title = U, n;
485
576
  }
486
577
  }
487
- const ie = o.exact({
488
- path: (i, e, r) => {
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 \`${r}\`, but its value is \`${n}\`.`
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 \`${r}\`, expected \`string\`.`
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 \`${r}\`. Route paths must start with a forward slash (/).`
586
+ `Invalid prop \`${e}\` value \`${n}\` supplied to \`${c}\`. Route paths must start with a forward slash (/).`
496
587
  );
497
588
  },
498
- element: o.element.isRequired,
499
- title: o.string,
500
- description: o.string,
501
- tags: o.arrayOf(o.object),
502
- imageUrl: o.string
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
- re.propTypes = {
505
- routes: o.arrayOf(ie),
506
- title: o.string,
507
- separator: o.string,
508
- notFoundTemplate: o.element,
509
- notFoundTitle: o.string,
510
- errorTemplate: o.element,
511
- errorTitle: o.string,
512
- useTrailingSlash: o.bool,
513
- usePrerenderTags: o.bool,
514
- baseUrl: (i, e, r) => {
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 \`${r}\`, expected \`string\`.`
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 \`${r}\`. The baseUrl should not end with a slash. Got: "${n}"`
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: o.string,
529
- touchIconUrl: o.string,
530
- debug: o.bool
619
+ imageUrl: t.string,
620
+ touchIconUrl: t.string,
621
+ debug: t.bool,
622
+ ignorePatterns: t.arrayOf(t.string)
531
623
  };
532
624
  export {
533
- Z as ErrorBoundary,
625
+ G as ErrorBoundary,
534
626
  oe as Image,
535
- re as Routerino,
536
- re as default,
537
- p as updateHeadTag,
538
- le as useRouterino
627
+ ie as Routerino,
628
+ ie as default,
629
+ h as updateHeadTag,
630
+ ue as useRouterino
539
631
  };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routerino",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "A lightweight, SEO-optimized React router for modern web applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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(([key]) => key !== "tag" && key !== "soft")
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
- tags.push(`<${tagName} ${attrs}>`);
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
  }
@@ -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., &lt;script&gt;, &lt;style&gt;) */
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 {