routerino 2.3.2 → 2.3.4

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/dist/routerino.js CHANGED
@@ -1,54 +1,162 @@
1
- import { jsx as g, jsxs as W, Fragment as A } from "react/jsx-runtime";
2
- import { createContext as O, Component as j, useContext as M, useState as _, useEffect as z } from "react";
1
+ import { jsx as g, jsxs as q, Fragment as F } from "react/jsx-runtime";
2
+ import { useState as I, useEffect as O, createContext as _, Component as V, useContext as G } from "react";
3
3
  import t from "prop-types";
4
- const F = O(null);
5
- function Y() {
6
- const i = M(F);
7
- if (!i)
4
+ const K = [480, 800, 1200, 1920], Z = "(max-width: 480px) 100vw, (max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px";
5
+ function J(r, e = "") {
6
+ const i = r.toLowerCase(), n = e.toLowerCase();
7
+ return !!(i.includes("hero") || i.includes("banner") || n.includes("hero") || n.includes("banner") || n.includes("h-screen") || n.includes("h-full"));
8
+ }
9
+ function H(r, e, i = null) {
10
+ const n = r.replace(/\.(jpe?g|png|webp)$/i, ""), h = i ? `.${i}` : r.match(/\.(jpe?g|png|webp)$/i)?.[0] || ".jpg";
11
+ return e.map((l) => `${n}-${l}w${h} ${l}w`).join(", ");
12
+ }
13
+ function B(r) {
14
+ const {
15
+ src: e = "",
16
+ alt: i = "",
17
+ priority: n,
18
+ widths: h = K,
19
+ sizes: l = Z,
20
+ className: p = "",
21
+ style: L = {},
22
+ loading: $,
23
+ decoding: E = "async",
24
+ fetchpriority: v,
25
+ ...x
26
+ } = r || {};
27
+ if (typeof window < "u" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"))
28
+ return /* @__PURE__ */ g(
29
+ "img",
30
+ {
31
+ src: e,
32
+ alt: i,
33
+ loading: $ || "lazy",
34
+ decoding: E,
35
+ fetchPriority: v,
36
+ className: p,
37
+ style: L,
38
+ ...x
39
+ }
40
+ );
41
+ const [T, D] = I(null), S = e && typeof window < "u";
42
+ O(() => {
43
+ if (!S) return;
44
+ const y = new B();
45
+ y.onload = () => {
46
+ D({
47
+ width: y.naturalWidth,
48
+ height: y.naturalHeight
49
+ });
50
+ }, y.src = e;
51
+ }, [e, S]);
52
+ const b = T ? h.filter((y) => T.width >= y) : h, [c, k] = I(b);
53
+ O(() => {
54
+ (async () => {
55
+ if (typeof window > "u") {
56
+ k(b);
57
+ return;
58
+ }
59
+ const U = e.replace(/\.(jpe?g|png|webp)$/i, ""), o = e.match(/\.(jpe?g|png|webp)$/i)?.[0] || ".jpg", a = [];
60
+ for (const w of b) {
61
+ const f = `${U}-${w}w${o}`;
62
+ try {
63
+ (await fetch(f, { method: "HEAD" })).ok && a.push(w);
64
+ } catch {
65
+ }
66
+ }
67
+ a.length === 0 && b.length > 0 && a.push(Math.min(...b)), k(a);
68
+ })();
69
+ }, [e, b]);
70
+ const P = n ?? J(e, p), W = $ || (P ? "eager" : "lazy"), s = v || (P ? "high" : void 0), j = H(e, c, "webp"), A = H(e, c);
71
+ return /* @__PURE__ */ q("picture", { "data-routerino-image": "true", "data-original-src": e, children: [
72
+ /* @__PURE__ */ g("source", { srcSet: j, type: "image/webp", sizes: l }),
73
+ /* @__PURE__ */ g(
74
+ "img",
75
+ {
76
+ src: e,
77
+ alt: i,
78
+ srcSet: A,
79
+ sizes: l,
80
+ loading: W,
81
+ decoding: E,
82
+ fetchPriority: s,
83
+ className: p,
84
+ style: L,
85
+ ...x
86
+ }
87
+ )
88
+ ] });
89
+ }
90
+ B.propTypes = {
91
+ /** Image source URL (required) */
92
+ src: t.string.isRequired,
93
+ /** Alt text for accessibility (required) */
94
+ alt: t.string.isRequired,
95
+ /** Override lazy loading for hero/LCP images */
96
+ priority: t.bool,
97
+ /** Responsive widths to generate (defaults to [480, 800, 1200, 1920]) */
98
+ widths: t.arrayOf(t.number),
99
+ /** Responsive sizes attribute (has smart default) */
100
+ sizes: t.string,
101
+ /** CSS classes (Tailwind/DaisyUI ready) */
102
+ className: t.string,
103
+ /** Inline styles */
104
+ style: t.object,
105
+ /** Loading behavior (auto-set based on priority) */
106
+ loading: t.oneOf(["lazy", "eager"]),
107
+ /** Decode timing */
108
+ decoding: t.oneOf(["sync", "async", "auto"]),
109
+ /** Fetch priority (auto-set based on priority) */
110
+ fetchpriority: t.oneOf(["high", "low", "auto"])
111
+ };
112
+ const z = _(null);
113
+ function oe() {
114
+ const r = G(z);
115
+ if (!r)
8
116
  throw new Error(
9
117
  "useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component."
10
118
  );
11
- return i;
119
+ return r;
12
120
  }
13
- function l({ tag: i = "meta", soft: c = !1, ...a }) {
14
- const o = Object.keys(a);
15
- if (o.length < 1)
121
+ function u({ tag: r = "meta", soft: e = !1, ...i }) {
122
+ const n = Object.keys(i);
123
+ if (n.length < 1)
16
124
  return console.error(
17
- `[Routerino] updateHeadTag() received no attributes to set for ${i} tag`
125
+ `[Routerino] updateHeadTag() received no attributes to set for ${r} tag`
18
126
  );
19
127
  let h = null;
20
- for (let f = 0; f < o.length && (o[f] !== "content" && (h = document.querySelector(
21
- `${i}[${o[f]}='${a[o[f]]}']`
22
- )), !h); f++)
128
+ for (let l = 0; l < n.length && (n[l] !== "content" && (h = document.querySelector(
129
+ `${r}[${n[l]}='${i[n[l]]}']`
130
+ )), !h); l++)
23
131
  ;
24
- h && c || (h || (h = document.createElement(i)), o.forEach((f) => h.setAttribute(f, a[f])), document.querySelector("head").appendChild(h));
132
+ h && e || (h || (h = document.createElement(r)), n.forEach((l) => h.setAttribute(l, i[l])), document.querySelector("head").appendChild(h));
25
133
  }
26
- function G({ routePattern: i, currentRoute: c }) {
27
- let a = {}, o = i.split("/"), h = c.split("/");
28
- return o.forEach((f, m) => {
29
- f.startsWith(":") && (a[f.slice(1)] = h[m]);
30
- }), a;
134
+ function N({ routePattern: r, currentRoute: e }) {
135
+ let i = {}, n = r.split("/"), h = e.split("/");
136
+ return n.forEach((l, p) => {
137
+ l.startsWith(":") && (i[l.slice(1)] = h[p]);
138
+ }), i;
31
139
  }
32
- class H extends j {
33
- constructor(c) {
34
- super(c), this.state = { hasError: !1 };
140
+ class M extends V {
141
+ constructor(e) {
142
+ super(e), this.state = { hasError: !1 };
35
143
  }
36
144
  static getDerivedStateFromError() {
37
145
  return { hasError: !0 };
38
146
  }
39
- componentDidCatch(c, a) {
147
+ componentDidCatch(e, i) {
40
148
  this.props.debug && (console.group(
41
149
  "%c[Routerino]%c Error Boundary Caught an Error",
42
150
  "color: #ff6b6b; font-weight: bold",
43
151
  "",
44
- c
45
- ), console.error("[Routerino] Component Stack:", a.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 && l({ name: "prerender-status-code", content: "500" });
152
+ e
153
+ ), console.error("[Routerino] Component Stack:", i.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 && u({ name: "prerender-status-code", content: "500" });
46
154
  }
47
155
  render() {
48
156
  return this.state.hasError ? this.props.fallback : this.props.children;
49
157
  }
50
158
  }
51
- H.propTypes = {
159
+ M.propTypes = {
52
160
  /** The child components to render when there's no error */
53
161
  children: t.node,
54
162
  /** The fallback UI to display when an error is caught */
@@ -62,8 +170,8 @@ H.propTypes = {
62
170
  /** Whether to log debug messages to console (optional) */
63
171
  debug: t.bool
64
172
  };
65
- function K({
66
- routes: i = [
173
+ function Q({
174
+ routes: r = [
67
175
  {
68
176
  path: "/",
69
177
  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." }),
@@ -72,148 +180,148 @@ function K({
72
180
  tags: [{ property: "og:locale", content: "en_US" }]
73
181
  }
74
182
  ],
75
- notFoundTemplate: c = /* @__PURE__ */ W(A, { children: [
183
+ notFoundTemplate: e = /* @__PURE__ */ q(F, { children: [
76
184
  /* @__PURE__ */ g("p", { children: "No page found for this URL. [404]" }),
77
185
  /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
78
186
  ] }),
79
- notFoundTitle: a = "Page not found [404]",
80
- errorTemplate: o = /* @__PURE__ */ W(A, { children: [
187
+ notFoundTitle: i = "Page not found [404]",
188
+ errorTemplate: n = /* @__PURE__ */ q(F, { children: [
81
189
  /* @__PURE__ */ g("p", { children: "Page failed to load. [500]" }),
82
190
  /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
83
191
  ] }),
84
192
  errorTitle: h = "Page error [500]",
85
- useTrailingSlash: f = !0,
86
- usePrerenderTags: m = !1,
87
- baseUrl: I = null,
88
- title: R = "",
89
- separator: y = " | ",
193
+ useTrailingSlash: l = !0,
194
+ usePrerenderTags: p = !1,
195
+ baseUrl: L = null,
196
+ title: $ = "",
197
+ separator: E = " | ",
90
198
  imageUrl: v = null,
91
- touchIconUrl: T = null,
92
- debug: s = !1
199
+ touchIconUrl: x = null,
200
+ debug: d = !1
93
201
  }) {
94
- const k = `${h}${y}${R}`, x = `${a}${y}${R}`;
202
+ const T = `${h}${E}${$}`, D = `${i}${E}${$}`;
95
203
  try {
96
- if (s) {
97
- const e = i.map((w) => w.path), u = e.filter(
98
- (w, d) => e.indexOf(w) !== d
204
+ if (d) {
205
+ const o = r.map((w) => w.path), a = o.filter(
206
+ (w, f) => o.indexOf(w) !== f
99
207
  );
100
- u.length > 0 && (console.warn(
208
+ a.length > 0 && (console.warn(
101
209
  "%c[Routerino]%c Duplicate route paths detected:",
102
210
  "color: #f59e0b; font-weight: bold",
103
211
  "",
104
- [...new Set(u)]
212
+ [...new Set(a)]
105
213
  ), console.warn(
106
214
  "%c[Routerino]%c The first matching route will be used",
107
215
  "color: #f59e0b; font-weight: bold",
108
216
  ""
109
217
  ));
110
218
  }
111
- const [$, P] = _(window?.location?.href ?? "/");
112
- z(() => {
219
+ const [S, b] = I(window?.location?.href ?? "/");
220
+ O(() => {
113
221
  if (typeof window > "u" || typeof document > "u")
114
222
  return;
115
- const e = (w) => {
116
- s && console.debug(
223
+ const o = (w) => {
224
+ d && console.debug(
117
225
  "%c[Routerino]%c click occurred",
118
226
  "color: #6b7280; font-weight: bold",
119
227
  ""
120
228
  );
121
- let d = w.target;
122
- for (; d.tagName !== "A" && d.parentElement; )
123
- d = d.parentElement;
124
- if (d.tagName !== "A") {
125
- s && console.debug(
229
+ let f = w.target;
230
+ for (; f.tagName !== "A" && f.parentElement; )
231
+ f = f.parentElement;
232
+ if (f.tagName !== "A") {
233
+ d && console.debug(
126
234
  "%c[Routerino]%c no anchor tag found during click",
127
235
  "color: #6b7280; font-weight: bold",
128
236
  ""
129
237
  );
130
238
  return;
131
239
  }
132
- const p = d.getAttribute("href") || d.href;
133
- if (!p) {
134
- s && console.debug(
240
+ const m = f.getAttribute("href") || f.href;
241
+ if (!m) {
242
+ d && console.debug(
135
243
  "%c[Routerino]%c anchor tag has no href",
136
244
  "color: #6b7280; font-weight: bold",
137
245
  ""
138
246
  );
139
247
  return;
140
248
  }
141
- if (!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(p)) {
142
- s && console.debug(
249
+ if (!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(m)) {
250
+ d && console.debug(
143
251
  "%c[Routerino]%c skipping non-http URL:",
144
252
  "color: #6b7280; font-weight: bold",
145
253
  "",
146
- p
254
+ m
147
255
  );
148
256
  return;
149
257
  }
150
- s && console.debug(
258
+ d && console.debug(
151
259
  "%c[Routerino]%c click target href:",
152
260
  "color: #6b7280; font-weight: bold",
153
261
  "",
154
- p
262
+ m
155
263
  );
156
- let b;
264
+ let R;
157
265
  try {
158
- b = new URL(p, window.location.href);
159
- } catch (S) {
160
- s && console.debug(
266
+ R = new URL(m, window.location.href);
267
+ } catch (C) {
268
+ d && console.debug(
161
269
  "%c[Routerino]%c Invalid URL:",
162
270
  "color: #6b7280; font-weight: bold",
163
271
  "",
164
- p,
165
- S
272
+ m,
273
+ C
166
274
  );
167
275
  return;
168
276
  }
169
- s && console.debug(
277
+ d && console.debug(
170
278
  "%c[Routerino]%c targetUrl:",
171
279
  "color: #6b7280; font-weight: bold",
172
280
  "",
173
- b,
281
+ R,
174
282
  "current:",
175
283
  window.location
176
- ), b && window.location.origin === b.origin ? (s && console.debug(
284
+ ), R && window.location.origin === R.origin ? (d && console.debug(
177
285
  "%c[Routerino]%c target link is same origin, will use push-state transitioning",
178
286
  "color: #6b7280; font-weight: bold",
179
287
  ""
180
- ), w.preventDefault(), d.href !== window.location.href && (P(d.href), window.history.pushState({}, "", d.href)), window.scrollTo({
288
+ ), w.preventDefault(), f.href !== window.location.href && (b(f.href), window.history.pushState({}, "", f.href)), window.scrollTo({
181
289
  top: 0,
182
290
  behavior: "auto"
183
- })) : s && console.debug(
291
+ })) : d && console.debug(
184
292
  "%c[Routerino]%c target link does not share an origin, standard browser link handling applies",
185
293
  "color: #6b7280; font-weight: bold",
186
294
  ""
187
295
  );
188
296
  };
189
- document.addEventListener("click", e);
190
- const u = () => {
191
- s && console.debug(
297
+ document.addEventListener("click", o);
298
+ const a = () => {
299
+ d && console.debug(
192
300
  "%c[Routerino]%c route change ->",
193
301
  "color: #6b7280; font-weight: bold",
194
302
  "",
195
303
  window.location.pathname
196
- ), P(window.location.href);
304
+ ), b(window.location.href);
197
305
  };
198
- return window.addEventListener("popstate", u), () => {
199
- document.removeEventListener("click", e), window.removeEventListener("popstate", u);
306
+ return window.addEventListener("popstate", a), () => {
307
+ document.removeEventListener("click", o), window.removeEventListener("popstate", a);
200
308
  };
201
- }, [$]);
202
- let r = window?.location?.pathname ?? "/";
203
- (r === "/index.html" || r === "") && (r = "/");
204
- const U = i.find((e) => e.path === r), C = i.find(
205
- (e) => `${e.path}/` === r || e.path === `${r}/`
206
- ), L = i.find((e) => {
207
- const u = e.path.endsWith("/") ? e.path.slice(0, -1) : e.path, w = r.endsWith("/") ? r.slice(0, -1) : r, d = u.split("/").filter(Boolean), p = w.split("/").filter(Boolean);
208
- return d.length !== p.length ? !1 : d.every((b, S) => b.startsWith(":") ? !0 : b === p[S]);
209
- }), n = U ?? C ?? L;
210
- if (s && console.debug(
309
+ }, [S]);
310
+ let c = window?.location?.pathname ?? "/";
311
+ (c === "/index.html" || c === "") && (c = "/");
312
+ const k = r.find((o) => o.path === c), P = r.find(
313
+ (o) => `${o.path}/` === c || o.path === `${c}/`
314
+ ), W = r.find((o) => {
315
+ const a = o.path.endsWith("/") ? o.path.slice(0, -1) : o.path, w = c.endsWith("/") ? c.slice(0, -1) : c, f = a.split("/").filter(Boolean), m = w.split("/").filter(Boolean);
316
+ return f.length !== m.length ? !1 : f.every((R, C) => R.startsWith(":") ? !0 : R === m[C]);
317
+ }), s = k ?? P ?? W;
318
+ if (d && console.debug(
211
319
  "%c[Routerino]%c Route matching:",
212
320
  "color: #6b7280; font-weight: bold",
213
321
  "",
214
- { match: n, exactMatch: U, addSlashMatch: C, paramsMatch: L }
215
- ), !n)
216
- return s && (console.group(
322
+ { match: s, exactMatch: k, addSlashMatch: P, paramsMatch: W }
323
+ ), !s)
324
+ return d && (console.group(
217
325
  "%c[Routerino]%c 404 - No matching route",
218
326
  "color: #f59e0b; font-weight: bold",
219
327
  ""
@@ -221,84 +329,84 @@ function K({
221
329
  "%c[Routerino]%c Requested path:",
222
330
  "color: #f59e0b; font-weight: bold",
223
331
  "",
224
- r
332
+ c
225
333
  ), console.warn(
226
334
  "%c[Routerino]%c Available routes:",
227
335
  "color: #f59e0b; font-weight: bold",
228
336
  "",
229
- i.map((e) => e.path)
230
- ), console.groupEnd()), document.title = x, m && l({ name: "prerender-status-code", content: "404" }), c;
231
- if (m) {
232
- const e = document.querySelector(
337
+ r.map((o) => o.path)
338
+ ), console.groupEnd()), document.title = D, p && u({ name: "prerender-status-code", content: "404" }), e;
339
+ if (p) {
340
+ const o = document.querySelector(
233
341
  'meta[name="prerender-status-code"]'
234
342
  );
235
- e && e.remove();
236
- const u = document.querySelector(
343
+ o && o.remove();
344
+ const a = document.querySelector(
237
345
  'meta[name="prerender-header"]'
238
346
  );
239
- u && u.remove();
347
+ a && a.remove();
240
348
  }
241
- const q = f && !r.endsWith("/") && r !== "/", B = !f && r.endsWith("/") && r !== "/", D = q ? `${r}/` : B ? r.slice(0, -1) : r, E = `${I ?? window?.location?.origin ?? ""}${D}`;
242
- if (n.title) {
243
- const e = `${n.title}${y}${R}`;
244
- document.title = e, l({
349
+ const j = l && !c.endsWith("/") && c !== "/", A = !l && c.endsWith("/") && c !== "/", y = j ? `${c}/` : A ? c.slice(0, -1) : c, U = `${L ?? window?.location?.origin ?? ""}${y}`;
350
+ if (s.title) {
351
+ const o = `${s.title}${E}${$}`;
352
+ document.title = o, u({
245
353
  tag: "link",
246
354
  rel: "canonical",
247
- href: E
248
- }), n.tags?.find(({ property: u }) => u === "og:title") || l({
355
+ href: U
356
+ }), s.tags?.find(({ property: a }) => a === "og:title") || u({
249
357
  property: "og:title",
250
- content: e
251
- }), n.tags?.find(({ property: u }) => u === "og:url") || l({
358
+ content: o
359
+ }), s.tags?.find(({ property: a }) => a === "og:url") || u({
252
360
  property: "og:url",
253
- content: E
361
+ content: U
254
362
  });
255
363
  }
256
- if (n.description && (l({ name: "description", content: n.description }), n.tags?.find(({ property: e }) => e === "og:description") || l({
364
+ if (s.description && (u({ name: "description", content: s.description }), s.tags?.find(({ property: o }) => o === "og:description") || u({
257
365
  property: "og:description",
258
- content: n.description
259
- })), (v || n.imageUrl) && l({
366
+ content: s.description
367
+ })), (v || s.imageUrl) && u({
260
368
  property: "og:image",
261
- content: n.imageUrl ?? v
262
- }), n.tags?.find(({ property: e }) => e === "twitter:card") || l({
369
+ content: s.imageUrl ?? v
370
+ }), s.tags?.find(({ property: o }) => o === "twitter:card") || u({
263
371
  name: "twitter:card",
264
372
  content: "summary_large_image"
265
- }), T && l({
373
+ }), x && u({
266
374
  tag: "link",
267
375
  rel: "apple-touch-icon",
268
- href: T
269
- }), m && (q || B) && (l({ name: "prerender-status-code", content: "301" }), l({
376
+ href: x
377
+ }), p && (j || A) && (u({ name: "prerender-status-code", content: "301" }), u({
270
378
  name: "prerender-header",
271
- content: `Location: ${E}`
272
- })), n.tags && n.tags.length ? (n.tags.find(({ property: e }) => e === "og:type") || l({ property: "og:type", content: "website" }), n.tags.forEach((e) => l(e))) : l({ property: "og:type", content: "website" }), n.element) {
273
- const e = G({
274
- routePattern: n.path,
275
- currentRoute: r
276
- }), u = {
277
- currentRoute: r,
278
- params: e,
279
- routePattern: n.path,
280
- updateHeadTag: l
379
+ content: `Location: ${U}`
380
+ })), s.tags && s.tags.length ? (s.tags.find(({ property: o }) => o === "og:type") || u({ property: "og:type", content: "website" }), s.tags.forEach((o) => u(o))) : u({ property: "og:type", content: "website" }), s.element) {
381
+ const o = N({
382
+ routePattern: s.path,
383
+ currentRoute: c
384
+ }), a = {
385
+ currentRoute: c,
386
+ params: o,
387
+ routePattern: s.path,
388
+ updateHeadTag: u
281
389
  };
282
- return /* @__PURE__ */ g(F.Provider, { value: u, children: /* @__PURE__ */ g(
283
- H,
390
+ return /* @__PURE__ */ g(z.Provider, { value: a, children: /* @__PURE__ */ g(
391
+ M,
284
392
  {
285
- fallback: o,
286
- errorTitleString: k,
287
- usePrerenderTags: m,
288
- routePath: r,
289
- debug: s,
290
- children: n.element
393
+ fallback: n,
394
+ errorTitleString: T,
395
+ usePrerenderTags: p,
396
+ routePath: c,
397
+ debug: d,
398
+ children: s.element
291
399
  }
292
400
  ) });
293
401
  }
294
- return s && console.error(
402
+ return d && console.error(
295
403
  "%c[Routerino]%c No route found for",
296
404
  "color: #ff6b6b; font-weight: bold",
297
405
  "",
298
- r
299
- ), document.title = x, m && l({ name: "prerender-status-code", content: "404" }), c;
300
- } catch ($) {
301
- return s && (console.group(
406
+ c
407
+ ), document.title = D, p && u({ name: "prerender-status-code", content: "404" }), e;
408
+ } catch (S) {
409
+ return d && (console.group(
302
410
  "%c[Routerino]%c Fatal Error",
303
411
  "color: #ff6b6b; font-weight: bold",
304
412
  ""
@@ -310,23 +418,23 @@ function K({
310
418
  "%c[Routerino]%c Error:",
311
419
  "color: #ff6b6b; font-weight: bold",
312
420
  "",
313
- $
421
+ S
314
422
  ), console.error(
315
423
  "%c[Routerino]%c This typically means an issue with route configuration or router setup",
316
424
  "color: #ff6b6b; font-weight: bold",
317
425
  ""
318
- ), console.groupEnd()), m && l({ name: "prerender-status-code", content: "500" }), document.title = k, o;
426
+ ), console.groupEnd()), p && u({ name: "prerender-status-code", content: "500" }), document.title = T, n;
319
427
  }
320
428
  }
321
- const J = t.exact({
322
- path: (i, c, a) => {
323
- const o = i[c];
324
- return o == null ? new Error(
325
- `The prop \`${c}\` is marked as required in \`${a}\`, but its value is \`${o}\`.`
326
- ) : typeof o != "string" ? new Error(
327
- `Invalid prop \`${c}\` of type \`${typeof o}\` supplied to \`${a}\`, expected \`string\`.`
328
- ) : o.startsWith("/") ? null : new Error(
329
- `Invalid prop \`${c}\` value \`${o}\` supplied to \`${a}\`. Route paths must start with a forward slash (/).`
429
+ const X = t.exact({
430
+ path: (r, e, i) => {
431
+ const n = r[e];
432
+ return n == null ? new Error(
433
+ `The prop \`${e}\` is marked as required in \`${i}\`, but its value is \`${n}\`.`
434
+ ) : typeof n != "string" ? new Error(
435
+ `Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${i}\`, expected \`string\`.`
436
+ ) : n.startsWith("/") ? null : new Error(
437
+ `Invalid prop \`${e}\` value \`${n}\` supplied to \`${i}\`. Route paths must start with a forward slash (/).`
330
438
  );
331
439
  },
332
440
  element: t.element.isRequired,
@@ -335,8 +443,8 @@ const J = t.exact({
335
443
  tags: t.arrayOf(t.object),
336
444
  imageUrl: t.string
337
445
  });
338
- K.propTypes = {
339
- routes: t.arrayOf(J),
446
+ Q.propTypes = {
447
+ routes: t.arrayOf(X),
340
448
  title: t.string,
341
449
  separator: t.string,
342
450
  notFoundTemplate: t.element,
@@ -345,16 +453,16 @@ K.propTypes = {
345
453
  errorTitle: t.string,
346
454
  useTrailingSlash: t.bool,
347
455
  usePrerenderTags: t.bool,
348
- baseUrl: (i, c, a) => {
349
- const o = i[c];
350
- if (o != null) {
351
- if (typeof o != "string")
456
+ baseUrl: (r, e, i) => {
457
+ const n = r[e];
458
+ if (n != null) {
459
+ if (typeof n != "string")
352
460
  return new Error(
353
- `Invalid prop \`${c}\` of type \`${typeof o}\` supplied to \`${a}\`, expected \`string\`.`
461
+ `Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${i}\`, expected \`string\`.`
354
462
  );
355
- if (o.endsWith("/"))
463
+ if (n.endsWith("/"))
356
464
  return new Error(
357
- `Invalid prop \`${c}\` supplied to \`${a}\`. The baseUrl should not end with a slash. Got: "${o}"`
465
+ `Invalid prop \`${e}\` supplied to \`${i}\`. The baseUrl should not end with a slash. Got: "${n}"`
358
466
  );
359
467
  }
360
468
  return null;
@@ -364,9 +472,10 @@ K.propTypes = {
364
472
  debug: t.bool
365
473
  };
366
474
  export {
367
- H as ErrorBoundary,
368
- K as Routerino,
369
- K as default,
370
- l as updateHeadTag,
371
- Y as useRouterino
475
+ M as ErrorBoundary,
476
+ B as Image,
477
+ Q as Routerino,
478
+ Q as default,
479
+ u as updateHeadTag,
480
+ oe as useRouterino
372
481
  };
@@ -1 +1 @@
1
- (function(g,a){typeof exports=="object"&&typeof module<"u"?a(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],a):(g=typeof globalThis<"u"?globalThis:g||self,a(g.routerino={},g["react/jsx-runtime"],g.React,g.PropTypes))})(this,function(g,a,$,t){"use strict";const q=$.createContext(null);function O(){const c=$.useContext(q);if(!c)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return c}function i({tag:c="meta",soft:l=!1,...s}){const o=Object.keys(s);if(o.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${c} tag`);let p=null;for(let h=0;h<o.length&&(o[h]!=="content"&&(p=document.querySelector(`${c}[${o[h]}='${s[o[h]]}']`)),!p);h++);p&&l||(p||(p=document.createElement(c)),o.forEach(h=>p.setAttribute(h,s[h])),document.querySelector("head").appendChild(p))}function D({routePattern:c,currentRoute:l}){let s={},o=c.split("/"),p=l.split("/");return o.forEach((h,b)=>{h.startsWith(":")&&(s[h.slice(1)]=p[b])}),s}class E extends $.Component{constructor(l){super(l),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(l,s){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",l),console.error("[Routerino] Component Stack:",s.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&&i({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}E.propTypes={children:t.node,fallback:t.node,errorTitleString:t.string.isRequired,usePrerenderTags:t.bool,routePath:t.string,debug:t.bool};function S({routes:c=[{path:"/",element:a.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:l=a.jsxs(a.Fragment,{children:[a.jsx("p",{children:"No page found for this URL. [404]"}),a.jsx("p",{children:a.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:s="Page not found [404]",errorTemplate:o=a.jsxs(a.Fragment,{children:[a.jsx("p",{children:"Page failed to load. [500]"}),a.jsx("p",{children:a.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:p="Page error [500]",useTrailingSlash:h=!0,usePrerenderTags:b=!1,baseUrl:z=null,title:v="",separator:y=" | ",imageUrl:C=null,touchIconUrl:B=null,debug:u=!1}){const L=`${p}${y}${v}`,W=`${s}${y}${v}`;try{if(u){const e=c.map(m=>m.path),d=e.filter((m,f)=>e.indexOf(m)!==f);d.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(d)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[k,F]=$.useState(window?.location?.href??"/");$.useEffect(()=>{if(typeof window>"u"||typeof document>"u")return;const e=m=>{u&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let f=m.target;for(;f.tagName!=="A"&&f.parentElement;)f=f.parentElement;if(f.tagName!=="A"){u&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}const w=f.getAttribute("href")||f.href;if(!w){u&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(w)){u&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",w);return}u&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",w);let R;try{R=new URL(w,window.location.href)}catch(U){u&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",w,U);return}u&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",R,"current:",window.location),R&&window.location.origin===R.origin?(u&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),m.preventDefault(),f.href!==window.location.href&&(F(f.href),window.history.pushState({},"",f.href)),window.scrollTo({top:0,behavior:"auto"})):u&&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",e);const d=()=>{u&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),F(window.location.href)};return window.addEventListener("popstate",d),()=>{document.removeEventListener("click",e),window.removeEventListener("popstate",d)}},[k]);let r=window?.location?.pathname??"/";(r==="/index.html"||r==="")&&(r="/");const H=c.find(e=>e.path===r),j=c.find(e=>`${e.path}/`===r||e.path===`${r}/`),A=c.find(e=>{const d=e.path.endsWith("/")?e.path.slice(0,-1):e.path,m=r.endsWith("/")?r.slice(0,-1):r,f=d.split("/").filter(Boolean),w=m.split("/").filter(Boolean);return f.length!==w.length?!1:f.every((R,U)=>R.startsWith(":")?!0:R===w[U])}),n=H??j??A;if(u&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:n,exactMatch:H,addSlashMatch:j,paramsMatch:A}),!n)return u&&(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","",r),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",c.map(e=>e.path)),console.groupEnd()),document.title=W,b&&i({name:"prerender-status-code",content:"404"}),l;if(b){const e=document.querySelector('meta[name="prerender-status-code"]');e&&e.remove();const d=document.querySelector('meta[name="prerender-header"]');d&&d.remove()}const I=h&&!r.endsWith("/")&&r!=="/",M=!h&&r.endsWith("/")&&r!=="/",T=I?`${r}/`:M?r.slice(0,-1):r,x=`${z??window?.location?.origin??""}${T}`;if(n.title){const e=`${n.title}${y}${v}`;document.title=e,i({tag:"link",rel:"canonical",href:x}),n.tags?.find(({property:d})=>d==="og:title")||i({property:"og:title",content:e}),n.tags?.find(({property:d})=>d==="og:url")||i({property:"og:url",content:x})}if(n.description&&(i({name:"description",content:n.description}),n.tags?.find(({property:e})=>e==="og:description")||i({property:"og:description",content:n.description})),(C||n.imageUrl)&&i({property:"og:image",content:n.imageUrl??C}),n.tags?.find(({property:e})=>e==="twitter:card")||i({name:"twitter:card",content:"summary_large_image"}),B&&i({tag:"link",rel:"apple-touch-icon",href:B}),b&&(I||M)&&(i({name:"prerender-status-code",content:"301"}),i({name:"prerender-header",content:`Location: ${x}`})),n.tags&&n.tags.length?(n.tags.find(({property:e})=>e==="og:type")||i({property:"og:type",content:"website"}),n.tags.forEach(e=>i(e))):i({property:"og:type",content:"website"}),n.element){const e=D({routePattern:n.path,currentRoute:r}),d={currentRoute:r,params:e,routePattern:n.path,updateHeadTag:i};return a.jsx(q.Provider,{value:d,children:a.jsx(E,{fallback:o,errorTitleString:L,usePrerenderTags:b,routePath:r,debug:u,children:n.element})})}return u&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",r),document.title=W,b&&i({name:"prerender-status-code",content:"404"}),l}catch(k){return u&&(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","",k),console.error("%c[Routerino]%c This typically means an issue with route configuration or router setup","color: #ff6b6b; font-weight: bold",""),console.groupEnd()),b&&i({name:"prerender-status-code",content:"500"}),document.title=L,o}}const _=t.exact({path:(c,l,s)=>{const o=c[l];return o==null?new Error(`The prop \`${l}\` is marked as required in \`${s}\`, but its value is \`${o}\`.`):typeof o!="string"?new Error(`Invalid prop \`${l}\` of type \`${typeof o}\` supplied to \`${s}\`, expected \`string\`.`):o.startsWith("/")?null:new Error(`Invalid prop \`${l}\` value \`${o}\` supplied to \`${s}\`. Route paths must start with a forward slash (/).`)},element:t.element.isRequired,title:t.string,description:t.string,tags:t.arrayOf(t.object),imageUrl:t.string});S.propTypes={routes:t.arrayOf(_),title:t.string,separator:t.string,notFoundTemplate:t.element,notFoundTitle:t.string,errorTemplate:t.element,errorTitle:t.string,useTrailingSlash:t.bool,usePrerenderTags:t.bool,baseUrl:(c,l,s)=>{const o=c[l];if(o!=null){if(typeof o!="string")return new Error(`Invalid prop \`${l}\` of type \`${typeof o}\` supplied to \`${s}\`, expected \`string\`.`);if(o.endsWith("/"))return new Error(`Invalid prop \`${l}\` supplied to \`${s}\`. The baseUrl should not end with a slash. Got: "${o}"`)}return null},imageUrl:t.string,touchIconUrl:t.string,debug:t.bool},g.ErrorBoundary=E,g.Routerino=S,g.default=S,g.updateHeadTag=i,g.useRouterino=O,Object.defineProperties(g,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ (function(p,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],l):(p=typeof globalThis<"u"?globalThis:p||self,l(p.routerino={},p["react/jsx-runtime"],p.React,p.PropTypes))})(this,function(p,l,$,e){"use strict";const V=[480,800,1200,1920],G="(max-width: 480px) 100vw, (max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px";function K(r,t=""){const i=r.toLowerCase(),n=t.toLowerCase();return!!(i.includes("hero")||i.includes("banner")||n.includes("hero")||n.includes("banner")||n.includes("h-screen")||n.includes("h-full"))}function M(r,t,i=null){const n=r.replace(/\.(jpe?g|png|webp)$/i,""),g=i?`.${i}`:r.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg";return t.map(u=>`${n}-${u}w${g} ${u}w`).join(", ")}function F(r){const{src:t="",alt:i="",priority:n,widths:g=V,sizes:u=G,className:w="",style:q={},loading:E,decoding:v="async",fetchpriority:k,...U}=r||{};if(typeof window<"u"&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1"))return l.jsx("img",{src:t,alt:i,loading:E||"lazy",decoding:v,fetchPriority:k,className:w,style:q,...U});const[L,A]=$.useState(null),x=t&&typeof window<"u";$.useEffect(()=>{if(!x)return;const S=new F;S.onload=()=>{A({width:S.naturalWidth,height:S.naturalHeight})},S.src=t},[t,x]);const R=L?g.filter(S=>L.width>=S):g,[c,D]=$.useState(R);$.useEffect(()=>{(async()=>{if(typeof window>"u"){D(R);return}const j=t.replace(/\.(jpe?g|png|webp)$/i,""),o=t.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg",s=[];for(const b of R){const h=`${j}-${b}w${o}`;try{(await fetch(h,{method:"HEAD"})).ok&&s.push(b)}catch{}}s.length===0&&R.length>0&&s.push(Math.min(...R)),D(s)})()},[t,R]);const W=n??K(t,w),C=E||(W?"eager":"lazy"),a=k||(W?"high":void 0),I=M(t,c,"webp"),O=M(t,c);return l.jsxs("picture",{"data-routerino-image":"true","data-original-src":t,children:[l.jsx("source",{srcSet:I,type:"image/webp",sizes:u}),l.jsx("img",{src:t,alt:i,srcSet:O,sizes:u,loading:C,decoding:v,fetchPriority:a,className:w,style:q,...U})]})}F.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,loading:e.oneOf(["lazy","eager"]),decoding:e.oneOf(["sync","async","auto"]),fetchpriority:e.oneOf(["high","low","auto"])};const _=$.createContext(null);function Z(){const r=$.useContext(_);if(!r)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return r}function d({tag:r="meta",soft:t=!1,...i}){const n=Object.keys(i);if(n.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${r} tag`);let g=null;for(let u=0;u<n.length&&(n[u]!=="content"&&(g=document.querySelector(`${r}[${n[u]}='${i[n[u]]}']`)),!g);u++);g&&t||(g||(g=document.createElement(r)),n.forEach(u=>g.setAttribute(u,i[u])),document.querySelector("head").appendChild(g))}function J({routePattern:r,currentRoute:t}){let i={},n=r.split("/"),g=t.split("/");return n.forEach((u,w)=>{u.startsWith(":")&&(i[u.slice(1)]=g[w])}),i}class H extends $.Component{constructor(t){super(t),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(t,i){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",t),console.error("[Routerino] Component Stack:",i.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}}H.propTypes={children:e.node,fallback:e.node,errorTitleString:e.string.isRequired,usePrerenderTags:e.bool,routePath:e.string,debug:e.bool};function B({routes:r=[{path:"/",element:l.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=l.jsxs(l.Fragment,{children:[l.jsx("p",{children:"No page found for this URL. [404]"}),l.jsx("p",{children:l.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:i="Page not found [404]",errorTemplate:n=l.jsxs(l.Fragment,{children:[l.jsx("p",{children:"Page failed to load. [500]"}),l.jsx("p",{children:l.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:g="Page error [500]",useTrailingSlash:u=!0,usePrerenderTags:w=!1,baseUrl:q=null,title:E="",separator:v=" | ",imageUrl:k=null,touchIconUrl:U=null,debug:f=!1}){const L=`${g}${v}${E}`,A=`${i}${v}${E}`;try{if(f){const o=r.map(b=>b.path),s=o.filter((b,h)=>o.indexOf(b)!==h);s.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(s)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[x,R]=$.useState(window?.location?.href??"/");$.useEffect(()=>{if(typeof window>"u"||typeof document>"u")return;const o=b=>{f&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let h=b.target;for(;h.tagName!=="A"&&h.parentElement;)h=h.parentElement;if(h.tagName!=="A"){f&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}const m=h.getAttribute("href")||h.href;if(!m){f&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(m)){f&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",m);return}f&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",m);let y;try{y=new URL(m,window.location.href)}catch(z){f&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",m,z);return}f&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",y,"current:",window.location),y&&window.location.origin===y.origin?(f&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),b.preventDefault(),h.href!==window.location.href&&(R(h.href),window.history.pushState({},"",h.href)),window.scrollTo({top:0,behavior:"auto"})):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 s=()=>{f&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),R(window.location.href)};return window.addEventListener("popstate",s),()=>{document.removeEventListener("click",o),window.removeEventListener("popstate",s)}},[x]);let c=window?.location?.pathname??"/";(c==="/index.html"||c==="")&&(c="/");const D=r.find(o=>o.path===c),W=r.find(o=>`${o.path}/`===c||o.path===`${c}/`),C=r.find(o=>{const s=o.path.endsWith("/")?o.path.slice(0,-1):o.path,b=c.endsWith("/")?c.slice(0,-1):c,h=s.split("/").filter(Boolean),m=b.split("/").filter(Boolean);return h.length!==m.length?!1:h.every((y,z)=>y.startsWith(":")?!0:y===m[z])}),a=D??W??C;if(f&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:a,exactMatch:D,addSlashMatch:W,paramsMatch:C}),!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","",c),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",r.map(o=>o.path)),console.groupEnd()),document.title=A,w&&d({name:"prerender-status-code",content:"404"}),t;if(w){const o=document.querySelector('meta[name="prerender-status-code"]');o&&o.remove();const s=document.querySelector('meta[name="prerender-header"]');s&&s.remove()}const I=u&&!c.endsWith("/")&&c!=="/",O=!u&&c.endsWith("/")&&c!=="/",S=I?`${c}/`:O?c.slice(0,-1):c,j=`${q??window?.location?.origin??""}${S}`;if(a.title){const o=`${a.title}${v}${E}`;document.title=o,d({tag:"link",rel:"canonical",href:j}),a.tags?.find(({property:s})=>s==="og:title")||d({property:"og:title",content:o}),a.tags?.find(({property:s})=>s==="og:url")||d({property:"og:url",content:j})}if(a.description&&(d({name:"description",content:a.description}),a.tags?.find(({property:o})=>o==="og:description")||d({property:"og:description",content:a.description})),(k||a.imageUrl)&&d({property:"og:image",content:a.imageUrl??k}),a.tags?.find(({property:o})=>o==="twitter:card")||d({name:"twitter:card",content:"summary_large_image"}),U&&d({tag:"link",rel:"apple-touch-icon",href:U}),w&&(I||O)&&(d({name:"prerender-status-code",content:"301"}),d({name:"prerender-header",content:`Location: ${j}`})),a.tags&&a.tags.length?(a.tags.find(({property:o})=>o==="og:type")||d({property:"og:type",content:"website"}),a.tags.forEach(o=>d(o))):d({property:"og:type",content:"website"}),a.element){const o=J({routePattern:a.path,currentRoute:c}),s={currentRoute:c,params:o,routePattern:a.path,updateHeadTag:d};return l.jsx(_.Provider,{value:s,children:l.jsx(H,{fallback:n,errorTitleString:L,usePrerenderTags:w,routePath:c,debug:f,children:a.element})})}return f&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",c),document.title=A,w&&d({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()),w&&d({name:"prerender-status-code",content:"500"}),document.title=L,n}}const N=e.exact({path:(r,t,i)=>{const n=r[t];return n==null?new Error(`The prop \`${t}\` is marked as required in \`${i}\`, but its value is \`${n}\`.`):typeof n!="string"?new Error(`Invalid prop \`${t}\` of type \`${typeof n}\` supplied to \`${i}\`, expected \`string\`.`):n.startsWith("/")?null:new Error(`Invalid prop \`${t}\` value \`${n}\` supplied to \`${i}\`. 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});B.propTypes={routes:e.arrayOf(N),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:(r,t,i)=>{const n=r[t];if(n!=null){if(typeof n!="string")return new Error(`Invalid prop \`${t}\` of type \`${typeof n}\` supplied to \`${i}\`, expected \`string\`.`);if(n.endsWith("/"))return new Error(`Invalid prop \`${t}\` supplied to \`${i}\`. The baseUrl should not end with a slash. Got: "${n}"`)}return null},imageUrl:e.string,touchIconUrl:e.string,debug:e.bool},p.ErrorBoundary=H,p.Image=F,p.Routerino=B,p.default=B,p.updateHeadTag=d,p.useRouterino=Z,Object.defineProperties(p,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routerino",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "A lightweight, SEO-optimized React router for modern web applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -70,6 +70,28 @@ export function Image(props) {
70
70
  ...rest
71
71
  } = props || {};
72
72
 
73
+ // In development, skip responsive images entirely to avoid 404s
74
+ const isDevelopment =
75
+ typeof window !== "undefined" &&
76
+ (window.location.hostname === "localhost" ||
77
+ window.location.hostname === "127.0.0.1");
78
+
79
+ if (isDevelopment) {
80
+ return (
81
+ <img
82
+ src={src}
83
+ alt={alt}
84
+ loading={explicitLoading || "lazy"}
85
+ decoding={decoding}
86
+ fetchPriority={explicitFetchPriority}
87
+ className={className}
88
+ style={style}
89
+ {...rest}
90
+ />
91
+ );
92
+ }
93
+
94
+ // Production mode: full responsive image functionality
73
95
  // Detect image dimensions to filter applicable widths
74
96
  const [imageDimensions, setImageDimensions] = useState(null);
75
97
 
@@ -94,6 +116,43 @@ export function Image(props) {
94
116
  ? widths.filter((width) => imageDimensions.width >= width)
95
117
  : widths; // Fallback to all widths during loading
96
118
 
119
+ // Check which responsive variants actually exist at runtime
120
+ const [availableWidths, setAvailableWidths] = useState(applicableWidths);
121
+
122
+ useEffect(() => {
123
+ const checkAvailableVariants = async () => {
124
+ if (typeof window === "undefined") {
125
+ setAvailableWidths(applicableWidths);
126
+ return;
127
+ }
128
+
129
+ const base = src.replace(/\.(jpe?g|png|webp)$/i, "");
130
+ const ext = src.match(/\.(jpe?g|png|webp)$/i)?.[0] || ".jpg";
131
+
132
+ const existingWidths = [];
133
+ for (const width of applicableWidths) {
134
+ const variantUrl = `${base}-${width}w${ext}`;
135
+ try {
136
+ const response = await fetch(variantUrl, { method: "HEAD" });
137
+ if (response.ok) {
138
+ existingWidths.push(width);
139
+ }
140
+ } catch {
141
+ // Variant doesn't exist, skip it
142
+ }
143
+ }
144
+
145
+ // Always include at least the smallest width if no variants exist
146
+ if (existingWidths.length === 0 && applicableWidths.length > 0) {
147
+ existingWidths.push(Math.min(...applicableWidths));
148
+ }
149
+
150
+ setAvailableWidths(existingWidths);
151
+ };
152
+
153
+ checkAvailableVariants();
154
+ }, [src, applicableWidths]);
155
+
97
156
  // Smart priority detection if not explicitly set
98
157
  const autoPriority = priority ?? shouldUsePriority(src, className);
99
158
 
@@ -102,9 +161,9 @@ export function Image(props) {
102
161
  const fetchPriority =
103
162
  explicitFetchPriority || (autoPriority ? "high" : undefined);
104
163
 
105
- // Generate srcsets for different formats using applicable widths only
106
- const srcSetWebP = generateSrcSet(src, applicableWidths, "webp");
107
- const srcSetOriginal = generateSrcSet(src, applicableWidths);
164
+ // Generate srcsets for different formats using available widths only
165
+ const srcSetWebP = generateSrcSet(src, availableWidths, "webp");
166
+ const srcSetOriginal = generateSrcSet(src, availableWidths);
108
167
 
109
168
  // For SSG: This will be processed by routerino-forge.js to include LQIP
110
169
  // The data-routerino-image attribute signals to the forge to process this