routerino 2.2.2 → 2.2.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/README.md CHANGED
@@ -708,7 +708,7 @@ export default defineConfig({
708
708
  - `/about/` → `about/index.html`
709
709
  - Canonical URLs and redirects based on `useTrailingSlash` setting
710
710
  - Prerender compatible 301 redirects for non-canonical versions
711
- - **Static host ready**: Output format aligns perfectly with Vercel, Netlify, and Cloudflare Pages conventions
711
+ - **Static host ready**: Output format aligns perfectly with any static host such as Netlify and Cloudflare Pages
712
712
  - Routes generate `/path/index.html` for clean URLs (and `/path.html` for compatibility with no-slash URLs)
713
713
  - `404.html` at root for custom error pages
714
714
  - No server configuration needed - just deploy!
@@ -1216,6 +1216,97 @@ import Routerino from "./vendor/routerino";
1216
1216
 
1217
1217
  By vendoring Routerino, you have full control over the code and can make any necessary modifications directly to the `routerino.jsx` file. However, keep in mind that you'll need to manually update the vendored file if you want to incorporate any future updates or bug fixes from the main Routerino repository.
1218
1218
 
1219
+ ## Accessibility & SEO Best Practices
1220
+
1221
+ To maximize your PageSpeed Insights and Lighthouse scores, we recommend setting up ESLint with accessibility rules. This helps catch common issues that hurt SEO and user experience.
1222
+
1223
+ ### Setting up eslint-plugin-jsx-a11y
1224
+
1225
+ 1. Install the plugin:
1226
+
1227
+ ```bash
1228
+ npm install --save-dev eslint-plugin-jsx-a11y
1229
+ ```
1230
+
1231
+ 2. Add to your ESLint config:
1232
+
1233
+ ```javascript
1234
+ // eslint.config.js (ESLint 9+ flat config)
1235
+ import jsxA11y from "eslint-plugin-jsx-a11y";
1236
+
1237
+ export default [
1238
+ {
1239
+ plugins: {
1240
+ "jsx-a11y": jsxA11y,
1241
+ },
1242
+ rules: {
1243
+ ...jsxA11y.configs.recommended.rules,
1244
+ },
1245
+ },
1246
+ ];
1247
+ ```
1248
+
1249
+ 3. Add a lint run script
1250
+
1251
+ ```json
1252
+ {
1253
+ "scripts": {
1254
+ "lint:": "eslint --ext .jsx,.js src/"
1255
+ }
1256
+ }
1257
+ ```
1258
+
1259
+ See https://github.com/jsx-eslint/eslint-plugin-jsx-a11y for more info.
1260
+
1261
+ ### Key Rules for Accessibility
1262
+
1263
+ 1. **Images**: Include descriptive `alt` text
1264
+
1265
+ ```jsx
1266
+ // ❌ Bad - Missing alt text
1267
+ <img src="/logo.png" />
1268
+
1269
+ // ✅ Good - Descriptive alt text
1270
+ <img src="/logo.png" alt="Company logo" />
1271
+
1272
+ // ✅ Good - Decorative images
1273
+ <img src="/decoration.png" alt="" role="presentation" />
1274
+ ```
1275
+
1276
+ 2. **Heading Hierarchy**: Use proper heading order
1277
+
1278
+ ```jsx
1279
+ // ❌ Bad - Skipping heading levels
1280
+ <h1>Page Title</h1>
1281
+ <h3>Subsection</h3> // Should be h2
1282
+
1283
+ // ✅ Good - Proper hierarchy
1284
+ <h1>Page Title</h1>
1285
+ <h2>Main Section</h2>
1286
+ <h3>Subsection</h3>
1287
+ ```
1288
+
1289
+ 3. **Link Text**: Avoid generic link text
1290
+
1291
+ ```jsx
1292
+ // ❌ Bad - Generic text
1293
+ <a href="/products">Click here</a>
1294
+
1295
+ // ✅ Good - Descriptive text
1296
+ <a href="/products">View our products</a>
1297
+ ```
1298
+
1299
+ 4. **ARIA Labels**: Use for icon-only buttons
1300
+
1301
+ ```jsx
1302
+ // ✅ Good - Icon button with label
1303
+ <button aria-label="Close dialog">
1304
+ <svg>...</svg>
1305
+ </button>
1306
+ ```
1307
+
1308
+ These practices will help you achieve better accessibility scores and ensure your site is accessible to all users.
1309
+
1219
1310
  ## Additional Resources
1220
1311
 
1221
1312
  Here are some sources for further reading on SEO best-practices.
package/dist/routerino.js CHANGED
@@ -1,9 +1,9 @@
1
- import { jsx as g, jsxs as j, Fragment as M } from "react/jsx-runtime";
2
- import { createContext as J, Component as Q, useContext as V, useState as X, useEffect as Y } from "react";
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";
3
3
  import t from "prop-types";
4
- const _ = J(null);
5
- function ne() {
6
- const i = V(_);
4
+ const F = O(null);
5
+ function Y() {
6
+ const i = M(F);
7
7
  if (!i)
8
8
  throw new Error(
9
9
  "useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component."
@@ -23,13 +23,13 @@ function l({ tag: i = "meta", soft: c = !1, ...a }) {
23
23
  ;
24
24
  h && c || (h || (h = document.createElement(i)), o.forEach((f) => h.setAttribute(f, a[f])), document.querySelector("head").appendChild(h));
25
25
  }
26
- function Z({ routePattern: i, currentRoute: c }) {
26
+ function G({ routePattern: i, currentRoute: c }) {
27
27
  let a = {}, o = i.split("/"), h = c.split("/");
28
28
  return o.forEach((f, m) => {
29
29
  f.startsWith(":") && (a[f.slice(1)] = h[m]);
30
30
  }), a;
31
31
  }
32
- class z extends Q {
32
+ class H extends j {
33
33
  constructor(c) {
34
34
  super(c), this.state = { hasError: !1 };
35
35
  }
@@ -48,7 +48,7 @@ class z extends Q {
48
48
  return this.state.hasError ? this.props.fallback : this.props.children;
49
49
  }
50
50
  }
51
- z.propTypes = {
51
+ H.propTypes = {
52
52
  /** The child components to render when there's no error */
53
53
  children: t.node,
54
54
  /** The fallback UI to display when an error is caught */
@@ -62,7 +62,7 @@ z.propTypes = {
62
62
  /** Whether to log debug messages to console (optional) */
63
63
  debug: t.bool
64
64
  };
65
- function N({
65
+ function K({
66
66
  routes: i = [
67
67
  {
68
68
  path: "/",
@@ -72,26 +72,25 @@ function N({
72
72
  tags: [{ property: "og:locale", content: "en_US" }]
73
73
  }
74
74
  ],
75
- notFoundTemplate: c = /* @__PURE__ */ j(M, { children: [
75
+ notFoundTemplate: c = /* @__PURE__ */ W(A, { children: [
76
76
  /* @__PURE__ */ g("p", { children: "No page found for this URL. [404]" }),
77
77
  /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
78
78
  ] }),
79
79
  notFoundTitle: a = "Page not found [404]",
80
- errorTemplate: o = /* @__PURE__ */ j(M, { children: [
80
+ errorTemplate: o = /* @__PURE__ */ W(A, { children: [
81
81
  /* @__PURE__ */ g("p", { children: "Page failed to load. [500]" }),
82
82
  /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
83
83
  ] }),
84
84
  errorTitle: h = "Page error [500]",
85
85
  useTrailingSlash: f = !0,
86
86
  usePrerenderTags: m = !1,
87
- baseUrl: G = null,
87
+ baseUrl: I = null,
88
88
  title: R = "",
89
89
  separator: y = " | ",
90
90
  imageUrl: v = null,
91
91
  touchIconUrl: T = null,
92
92
  debug: s = !1
93
93
  }) {
94
- var P, U, C, L, q, B, W;
95
94
  const k = `${h}${y}${R}`, x = `${a}${y}${R}`;
96
95
  try {
97
96
  if (s) {
@@ -109,8 +108,8 @@ function N({
109
108
  ""
110
109
  ));
111
110
  }
112
- const [$, A] = X(((P = window == null ? void 0 : window.location) == null ? void 0 : P.href) ?? "/");
113
- Y(() => {
111
+ const [$, P] = _(window?.location?.href ?? "/");
112
+ z(() => {
114
113
  if (typeof window > "u" || typeof document > "u")
115
114
  return;
116
115
  const e = (w) => {
@@ -178,7 +177,7 @@ function N({
178
177
  "%c[Routerino]%c target link is same origin, will use push-state transitioning",
179
178
  "color: #6b7280; font-weight: bold",
180
179
  ""
181
- ), w.preventDefault(), d.href !== window.location.href && (A(d.href), window.history.pushState({}, "", d.href)), window.scrollTo({
180
+ ), w.preventDefault(), d.href !== window.location.href && (P(d.href), window.history.pushState({}, "", d.href)), window.scrollTo({
182
181
  top: 0,
183
182
  behavior: "auto"
184
183
  })) : s && console.debug(
@@ -194,25 +193,25 @@ function N({
194
193
  "color: #6b7280; font-weight: bold",
195
194
  "",
196
195
  window.location.pathname
197
- ), A(window.location.href);
196
+ ), P(window.location.href);
198
197
  };
199
198
  return window.addEventListener("popstate", u), () => {
200
199
  document.removeEventListener("click", e), window.removeEventListener("popstate", u);
201
200
  };
202
201
  }, [$]);
203
- let r = ((U = window == null ? void 0 : window.location) == null ? void 0 : U.pathname) ?? "/";
202
+ let r = window?.location?.pathname ?? "/";
204
203
  (r === "/index.html" || r === "") && (r = "/");
205
- const F = i.find((e) => e.path === r), H = i.find(
204
+ const U = i.find((e) => e.path === r), C = i.find(
206
205
  (e) => `${e.path}/` === r || e.path === `${r}/`
207
- ), I = i.find((e) => {
206
+ ), L = i.find((e) => {
208
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);
209
208
  return d.length !== p.length ? !1 : d.every((b, S) => b.startsWith(":") ? !0 : b === p[S]);
210
- }), n = F ?? H ?? I;
209
+ }), n = U ?? C ?? L;
211
210
  if (s && console.debug(
212
211
  "%c[Routerino]%c Route matching:",
213
212
  "color: #6b7280; font-weight: bold",
214
213
  "",
215
- { match: n, exactMatch: F, addSlashMatch: H, paramsMatch: I }
214
+ { match: n, exactMatch: U, addSlashMatch: C, paramsMatch: L }
216
215
  ), !n)
217
216
  return s && (console.group(
218
217
  "%c[Routerino]%c 404 - No matching route",
@@ -239,39 +238,39 @@ function N({
239
238
  );
240
239
  u && u.remove();
241
240
  }
242
- const D = f && !r.endsWith("/") && r !== "/", O = !f && r.endsWith("/") && r !== "/", K = D ? `${r}/` : O ? r.slice(0, -1) : r, E = `${G ?? ((C = window == null ? void 0 : window.location) == null ? void 0 : C.origin) ?? ""}${K}`;
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}`;
243
242
  if (n.title) {
244
243
  const e = `${n.title}${y}${R}`;
245
244
  document.title = e, l({
246
245
  tag: "link",
247
246
  rel: "canonical",
248
247
  href: E
249
- }), (L = n.tags) != null && L.find(({ property: u }) => u === "og:title") || l({
248
+ }), n.tags?.find(({ property: u }) => u === "og:title") || l({
250
249
  property: "og:title",
251
250
  content: e
252
- }), (q = n.tags) != null && q.find(({ property: u }) => u === "og:url") || l({
251
+ }), n.tags?.find(({ property: u }) => u === "og:url") || l({
253
252
  property: "og:url",
254
253
  content: E
255
254
  });
256
255
  }
257
- if (n.description && (l({ name: "description", content: n.description }), (B = n.tags) != null && B.find(({ property: e }) => e === "og:description") || l({
256
+ if (n.description && (l({ name: "description", content: n.description }), n.tags?.find(({ property: e }) => e === "og:description") || l({
258
257
  property: "og:description",
259
258
  content: n.description
260
259
  })), (v || n.imageUrl) && l({
261
260
  property: "og:image",
262
261
  content: n.imageUrl ?? v
263
- }), (W = n.tags) != null && W.find(({ property: e }) => e === "twitter:card") || l({
262
+ }), n.tags?.find(({ property: e }) => e === "twitter:card") || l({
264
263
  name: "twitter:card",
265
264
  content: "summary_large_image"
266
265
  }), T && l({
267
266
  tag: "link",
268
267
  rel: "apple-touch-icon",
269
268
  href: T
270
- }), m && (D || O) && (l({ name: "prerender-status-code", content: "301" }), l({
269
+ }), m && (q || B) && (l({ name: "prerender-status-code", content: "301" }), l({
271
270
  name: "prerender-header",
272
271
  content: `Location: ${E}`
273
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) {
274
- const e = Z({
273
+ const e = G({
275
274
  routePattern: n.path,
276
275
  currentRoute: r
277
276
  }), u = {
@@ -280,8 +279,8 @@ function N({
280
279
  routePattern: n.path,
281
280
  updateHeadTag: l
282
281
  };
283
- return /* @__PURE__ */ g(_.Provider, { value: u, children: /* @__PURE__ */ g(
284
- z,
282
+ return /* @__PURE__ */ g(F.Provider, { value: u, children: /* @__PURE__ */ g(
283
+ H,
285
284
  {
286
285
  fallback: o,
287
286
  errorTitleString: k,
@@ -319,7 +318,7 @@ function N({
319
318
  ), console.groupEnd()), m && l({ name: "prerender-status-code", content: "500" }), document.title = k, o;
320
319
  }
321
320
  }
322
- const ee = t.exact({
321
+ const J = t.exact({
323
322
  path: (i, c, a) => {
324
323
  const o = i[c];
325
324
  return o == null ? new Error(
@@ -336,8 +335,8 @@ const ee = t.exact({
336
335
  tags: t.arrayOf(t.object),
337
336
  imageUrl: t.string
338
337
  });
339
- N.propTypes = {
340
- routes: t.arrayOf(ee),
338
+ K.propTypes = {
339
+ routes: t.arrayOf(J),
341
340
  title: t.string,
342
341
  separator: t.string,
343
342
  notFoundTemplate: t.element,
@@ -365,9 +364,9 @@ N.propTypes = {
365
364
  debug: t.bool
366
365
  };
367
366
  export {
368
- z as ErrorBoundary,
369
- N as Routerino,
370
- N as default,
367
+ H as ErrorBoundary,
368
+ K as Routerino,
369
+ K as default,
371
370
  l as updateHeadTag,
372
- ne as useRouterino
371
+ Y as useRouterino
373
372
  };
@@ -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 J(){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 Q({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:X=null,title:v="",separator:y=" | ",imageUrl:C=null,touchIconUrl:B=null,debug:u=!1}){var F,H,j,A,I,M,O;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,D]=$.useState(((F=window==null?void 0:window.location)==null?void 0:F.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&&(D(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),D(window.location.href)};return window.addEventListener("popstate",d),()=>{document.removeEventListener("click",e),window.removeEventListener("popstate",d)}},[k]);let r=((H=window==null?void 0:window.location)==null?void 0:H.pathname)??"/";(r==="/index.html"||r==="")&&(r="/");const _=c.find(e=>e.path===r),z=c.find(e=>`${e.path}/`===r||e.path===`${r}/`),T=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=_??z??T;if(u&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:n,exactMatch:_,addSlashMatch:z,paramsMatch:T}),!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 G=h&&!r.endsWith("/")&&r!=="/",K=!h&&r.endsWith("/")&&r!=="/",Y=G?`${r}/`:K?r.slice(0,-1):r,x=`${X??((j=window==null?void 0:window.location)==null?void 0:j.origin)??""}${Y}`;if(n.title){const e=`${n.title}${y}${v}`;document.title=e,i({tag:"link",rel:"canonical",href:x}),(A=n.tags)!=null&&A.find(({property:d})=>d==="og:title")||i({property:"og:title",content:e}),(I=n.tags)!=null&&I.find(({property:d})=>d==="og:url")||i({property:"og:url",content:x})}if(n.description&&(i({name:"description",content:n.description}),(M=n.tags)!=null&&M.find(({property:e})=>e==="og:description")||i({property:"og:description",content:n.description})),(C||n.imageUrl)&&i({property:"og:image",content:n.imageUrl??C}),(O=n.tags)!=null&&O.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&&(G||K)&&(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=Q({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 V=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(V),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=J,Object.defineProperties(g,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
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"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routerino",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "A lightweight, SEO-optimized React router for modern web applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,24 +55,24 @@
55
55
  "prepare": "husky"
56
56
  },
57
57
  "devDependencies": {
58
- "@eslint/js": "^9.27.0",
58
+ "@eslint/js": "^9.38.0",
59
59
  "@testing-library/react": "^16.3.0",
60
60
  "@testing-library/user-event": "^14.6.1",
61
- "@vitejs/plugin-react": "^4.5.0",
62
- "eslint": "^9.27.0",
61
+ "@vitejs/plugin-react": "^5.0.4",
62
+ "eslint": "^9.38.0",
63
63
  "eslint-plugin-react": "^7.37.5",
64
- "express": "^4.18.2",
65
- "globals": "^16.1.0",
64
+ "express": "^5.1.0",
65
+ "globals": "^16.4.0",
66
66
  "husky": "^9.1.7",
67
67
  "jsdom": "^26.1.0",
68
- "lint-staged": "^16.1.2",
68
+ "lint-staged": "^16.2.6",
69
69
  "node-fetch": "^3.3.2",
70
70
  "prettier": "^3.6.2",
71
71
  "prop-types": "^15.8.1",
72
- "react": "^19.1.0",
73
- "react-dom": "^19.1.0",
74
- "vite": "^6.3.5",
75
- "vitest": "^3.2.4"
72
+ "react": "^19.2.0",
73
+ "react-dom": "^19.2.0",
74
+ "vite": "^7.1.12",
75
+ "vitest": "^4.0.2"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "prop-types": "^15.0.0",
@@ -91,8 +91,8 @@
91
91
  "node": ">=18"
92
92
  },
93
93
  "volta": {
94
- "node": "22.16.0",
95
- "npm": "10.9.2"
94
+ "node": "22.21.0",
95
+ "npm": "10.9.4"
96
96
  },
97
97
  "lint-staged": {
98
98
  "*.{js,jsx,mjs,cjs}": [
@@ -1230,5 +1230,4 @@ Sitemap: ${config.baseUrl}/sitemap.xml`;
1230
1230
  }
1231
1231
  }
1232
1232
 
1233
- // Default export
1234
1233
  export default routerinoForge;