routerino 1.1.9 → 1.2.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 +189 -109
- package/build-static.js +270 -0
- package/dist/routerino.js +182 -110
- package/dist/routerino.umd.cjs +1 -1
- package/package.json +48 -9
- package/types/routerino.d.ts +31 -0
package/README.md
CHANGED
|
@@ -2,13 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
> A lightweight, SEO-optimized React router for modern websites and applications
|
|
4
4
|
|
|
5
|
-
Routerino is a zero-dependency router
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
Routerino is a zero-dependency router for React (17/18/19) designed for optimal SEO performance in client-side rendered applications. Built for modern web architectures like JAMStack applications and Vite-powered React sites, it provides route & meta tag management, sitemap generation, and static site generation or [prerender](https://github.com/prerender/prerender) support to ensure your React applications are fully discoverable by search engines.
|
|
6
|
+
|
|
7
|
+
## Why Routerino?
|
|
8
|
+
|
|
9
|
+
- **SEO-First Design**: Automatic meta tag management, sitemap generation, and prerender support ensure maximum search engine visibility
|
|
10
|
+
- **Zero Dependencies**: Keeps bundle size minimal and reduces supply-chain vulnerabilities
|
|
11
|
+
- **Simple API**: No special `Link` components required - use standard HTML anchors and navigate programmatically with standard browser APIs
|
|
12
|
+
- **Static Site Generation**: Build-tool agnostic static HTML generation for improved performance and SEO
|
|
13
|
+
- **Production Ready**: Includes Docker-based prerender server for easy deployments
|
|
14
|
+
- **Single File Core**: The entire routing logic fits in one file (~420 lines), making it easy to understand and customize
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Features](#features)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Usage](#usage)
|
|
21
|
+
- [Props](#props-arguments)
|
|
22
|
+
- [Get route parameters](#get-route-parameters-and-the-current-route-and-updating-head-tags)
|
|
23
|
+
- [updateHeadTag](#updateheadtag)
|
|
24
|
+
- [Best Practices](#routerino-best-practices)
|
|
25
|
+
- [Generating a Sitemap](#generating-a-sitemap-from-routes)
|
|
26
|
+
- [Static Site Generation](#static-site-generation)
|
|
27
|
+
- [Deployment Guides](#deployment-guides)
|
|
28
|
+
- [Prerender Server (Docker)](#prerender-server-docker)
|
|
29
|
+
- [How-to Guides & Examples](#how-to-guides--example-code)
|
|
30
|
+
- [Starting a New Project](#starting-a-new-react-project-with-routerino)
|
|
31
|
+
- [Full React Example](#full-react-example)
|
|
32
|
+
- [Basic Example](#basic-example)
|
|
33
|
+
- [ErrorBoundary Component](#errorboundary-component)
|
|
34
|
+
- [Vendoring Routerino](#vendoring-routerino)
|
|
35
|
+
- [Additional Resources](#additional-resources)
|
|
36
|
+
- [Contributions](#contributions)
|
|
37
|
+
- [License](#license)
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
12
40
|
|
|
13
41
|
```jsx
|
|
14
42
|
<Routerino
|
|
@@ -36,36 +64,28 @@ Here's a quick example of what using Routerino looks like:
|
|
|
36
64
|
/>
|
|
37
65
|
```
|
|
38
66
|
|
|
39
|
-
|
|
67
|
+
This simple configuration automatically handles routing, meta tags, and SEO optimization for your React application.
|
|
40
68
|
|
|
41
69
|
## Features
|
|
42
70
|
|
|
43
|
-
Routerino empowers developers to define and manage routing and SEO concerns in one centralized location. This approach eliminates duplication when creating sitemaps and setting page metadata, such as descriptions or open-graph tags. The core of Routerino fits in a single file, making it [easy to vendor](#vendoring-routerino) if that suits your needs.
|
|
44
|
-
|
|
45
|
-
Key capabilities:
|
|
46
|
-
|
|
47
71
|
- Routing
|
|
48
|
-
|
|
49
|
-
- Easy integration of simple routing for your React app (supports React v18, older versions have not yet been tested)
|
|
72
|
+
- Easy integration of simple routing for your React app (supports React versions 17, 18, and 19)
|
|
50
73
|
- Zero dependencies for lighter, more maintainable projects
|
|
51
74
|
- No special link components required, works great for Markdown-based pages and semantic HTML
|
|
52
75
|
|
|
53
76
|
- SEO Optimization
|
|
54
|
-
|
|
55
77
|
- Configure title, description, and image for each route
|
|
56
78
|
- Set `<head>` tags for any route (either directly in your routes config, or dynamically after rendering)
|
|
57
79
|
- Set a site-wide name to be included with page titles
|
|
58
80
|
- Automatically generate and maintain an up-to-date `sitemap.xml` from your routes
|
|
81
|
+
- Generate static HTML files for each route with proper meta tags
|
|
59
82
|
- Implement SEO best practices out-of-the-box
|
|
60
83
|
- Optimize for Googlebot with pre-rendering support
|
|
61
84
|
|
|
62
85
|
- Enhanced User Experience
|
|
63
|
-
|
|
64
86
|
- Support for sharing and social preview metadata
|
|
65
87
|
- Snappy page transitions with automatic scroll reset, eliminating the jarring experience of landing mid-page when navigating
|
|
66
88
|
|
|
67
|
-
Routerino is designed to work with modern browsers and has been tested with the latest versions of Chrome, Firefox, Safari, and Edge.
|
|
68
|
-
|
|
69
89
|
## Installation
|
|
70
90
|
|
|
71
91
|
Ensure that you have React and React DOM installed in your project as peer dependencies. To add as a dev dependency:
|
|
@@ -74,6 +94,14 @@ Ensure that you have React and React DOM installed in your project as peer depen
|
|
|
74
94
|
npm i routerino -D
|
|
75
95
|
```
|
|
76
96
|
|
|
97
|
+
### Compatibility
|
|
98
|
+
|
|
99
|
+
Routerino supports:
|
|
100
|
+
|
|
101
|
+
- **React 17, 18, and 19** - All versions are tested and supported
|
|
102
|
+
- **Preact** - Compatible via `@preact/compat` (needs verification)
|
|
103
|
+
- **Node.js 18+** - Tested on Node.js 18, 20, and 22. Can run on earlier versions if we drop tests.
|
|
104
|
+
|
|
77
105
|
## Usage
|
|
78
106
|
|
|
79
107
|
Here's a quick example of using Routerino in your React application:
|
|
@@ -92,7 +120,7 @@ Here's a quick example of using Routerino in your React application:
|
|
|
92
120
|
/>
|
|
93
121
|
```
|
|
94
122
|
|
|
95
|
-
Links are just regular HTML anchor tags. No need to use special `<Link>` components and you can handle styling however you wish. For example: `<a href="/some-page
|
|
123
|
+
Links are just regular HTML anchor tags. No need to use special `<Link>` components and you can handle styling however you wish. For example: `<a href="/some-page/">a link</a>`
|
|
96
124
|
|
|
97
125
|
See [props](#props-arguments) for full explanations and [example code](#how-to-guides--example-code) for more complete code samples.
|
|
98
126
|
|
|
@@ -138,7 +166,7 @@ See [RouteConfig props](#routeconfig-props) for more details. At a minimum a pat
|
|
|
138
166
|
|
|
139
167
|
##### `separator`: string;
|
|
140
168
|
|
|
141
|
-
A string to separate the page title from the site title. The default is
|
|
169
|
+
A string to separate the page title from the site title. The default is `" | "` (a pipe character w/space around). Set this to customize the separator.
|
|
142
170
|
|
|
143
171
|
##### `notFoundTemplate`: element;
|
|
144
172
|
|
|
@@ -405,11 +433,84 @@ Add `routerino-build-sitemap` to your build command to update automatically on e
|
|
|
405
433
|
|
|
406
434
|
Example package.json build script: `"build": "vite build && routerino-build-sitemap routeFilePath=src/routes.jsx hostname=https://example.com outputDir=dist",`
|
|
407
435
|
|
|
436
|
+
## Static Site Generation
|
|
437
|
+
|
|
438
|
+
Routerino includes a build-tool agnostic static site generator that creates HTML files for each route, improving SEO and initial page load performance.
|
|
439
|
+
|
|
440
|
+
### How It Works
|
|
441
|
+
|
|
442
|
+
The `routerino-build-static` command is a **post-build step** that works with ANY build tool (Vite, Webpack, Parcel, etc.):
|
|
443
|
+
|
|
444
|
+
1. **First**: Build your app with your preferred build tool (`npm run build`)
|
|
445
|
+
2. **Then**: Run `routerino-build-static` to generate static HTML files
|
|
446
|
+
|
|
447
|
+
```sh
|
|
448
|
+
# After your build completes (creates dist/index.html with bundled JS/CSS):
|
|
449
|
+
routerino-build-static routesFile=src/routes.jsx outputDir=dist template=dist/index.html baseUrl=https://example.com
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Parameters:**
|
|
453
|
+
|
|
454
|
+
- `routesFile` - Path to your routes configuration file (supports .js, .jsx, .ts, .tsx)
|
|
455
|
+
- `outputDir` - Directory where static HTML files will be generated (usually your build output)
|
|
456
|
+
- `template` - Your **built** HTML file with bundled assets (e.g., dist/index.html)
|
|
457
|
+
- `baseUrl` - Base URL for meta tags (optional but recommended for SEO)
|
|
458
|
+
|
|
459
|
+
### Build Tool Examples
|
|
460
|
+
|
|
461
|
+
Works with any build tool:
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
// Vite
|
|
465
|
+
"build": "vite build && routerino-build-static routesFile=src/routes.js outputDir=dist template=dist/index.html"
|
|
466
|
+
|
|
467
|
+
// Webpack
|
|
468
|
+
"build": "webpack && routerino-build-static routesFile=src/routes.js outputDir=build template=build/index.html"
|
|
469
|
+
|
|
470
|
+
// Parcel
|
|
471
|
+
"build": "parcel build index.html && routerino-build-static routesFile=src/routes.js outputDir=dist template=dist/index.html"
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### What Gets Generated
|
|
475
|
+
|
|
476
|
+
The static build process will:
|
|
477
|
+
|
|
478
|
+
- Generate an HTML file for each non-dynamic route (routes with `:param` are skipped)
|
|
479
|
+
- Apply route-specific meta tags (title, description, og:tags, custom tags)
|
|
480
|
+
- Add proper `data-route` attributes for client-side hydration
|
|
481
|
+
- Preserve your existing HTML structure and assets
|
|
482
|
+
|
|
483
|
+
### Example Output
|
|
484
|
+
|
|
485
|
+
For a route configuration like:
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
{
|
|
489
|
+
path: '/about',
|
|
490
|
+
title: 'About Us',
|
|
491
|
+
description: 'Learn more about our company',
|
|
492
|
+
imageUrl: 'https://example.com/about-og.jpg'
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
The generated `/about.html` will include:
|
|
497
|
+
|
|
498
|
+
```html
|
|
499
|
+
<title>About Us</title>
|
|
500
|
+
<meta name="description" content="Learn more about our company" />
|
|
501
|
+
<meta property="og:title" content="About Us" />
|
|
502
|
+
<meta property="og:description" content="Learn more about our company" />
|
|
503
|
+
<meta property="og:image" content="https://example.com/about-og.jpg" />
|
|
504
|
+
<meta property="og:url" content="https://example.com/about" />
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
This provides excellent SEO while maintaining the benefits of a React SPA.
|
|
508
|
+
|
|
408
509
|
## How-to Guides & Example Code
|
|
409
510
|
|
|
410
511
|
1. [Starting a New React Project with Routerino](#starting-a-new-react-project-with-routerino)
|
|
411
|
-
2. [
|
|
412
|
-
3. [
|
|
512
|
+
2. [Full React Example](#full-react-example)
|
|
513
|
+
3. [Basic Example](#basic-example)
|
|
413
514
|
|
|
414
515
|
### Starting a New React Project with Routerino
|
|
415
516
|
|
|
@@ -457,40 +558,15 @@ This command will install the latest version of Routerino and save it to your `p
|
|
|
457
558
|
|
|
458
559
|
With these steps, you'll have a new React project set up with Vite as the build tool and Routerino installed as a development dependency. You can now start building your application with React & Routerino.
|
|
459
560
|
|
|
460
|
-
###
|
|
561
|
+
### Full React Example
|
|
461
562
|
|
|
462
|
-
|
|
563
|
+
This example includes the full React configuration. It might take the place of `src/main.jsx` or an `index.js` file.
|
|
463
564
|
|
|
464
565
|
```jsx
|
|
465
566
|
import React from "react";
|
|
567
|
+
import { render } from "react-dom";
|
|
466
568
|
import Routerino from "routerino";
|
|
467
569
|
|
|
468
|
-
// example pages
|
|
469
|
-
import HomePage from "./HomePage";
|
|
470
|
-
import AboutPage from "./AboutPage";
|
|
471
|
-
import ContactPage from "./ContactPage";
|
|
472
|
-
|
|
473
|
-
const routes = [
|
|
474
|
-
{
|
|
475
|
-
path: "/",
|
|
476
|
-
element: <HomePage />,
|
|
477
|
-
title: "Home",
|
|
478
|
-
description: "Welcome to my website!",
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
path: "/about/",
|
|
482
|
-
element: <AboutPage />,
|
|
483
|
-
title: "About",
|
|
484
|
-
description: "Learn more about us.",
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
path: "/contact/",
|
|
488
|
-
element: <ContactPage />,
|
|
489
|
-
title: "Contact",
|
|
490
|
-
description: "Get in touch with us.",
|
|
491
|
-
},
|
|
492
|
-
];
|
|
493
|
-
|
|
494
570
|
const App = () => (
|
|
495
571
|
<main>
|
|
496
572
|
<nav>
|
|
@@ -498,10 +574,37 @@ const App = () => (
|
|
|
498
574
|
</nav>
|
|
499
575
|
|
|
500
576
|
<Routerino
|
|
501
|
-
title="
|
|
502
|
-
routes={routes}
|
|
577
|
+
title="Example.com"
|
|
503
578
|
notFoundTitle="Sorry, but this page does not exist."
|
|
504
579
|
errorTitle="Yikes! Something went wrong."
|
|
580
|
+
routes={[
|
|
581
|
+
{
|
|
582
|
+
path: "/",
|
|
583
|
+
element: <p>Welcome to Home</p>,
|
|
584
|
+
title: "Home",
|
|
585
|
+
description: "Welcome to my website!",
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
path: "/about/",
|
|
589
|
+
element: <p>About us...</p>,
|
|
590
|
+
title: "About",
|
|
591
|
+
description: "Learn more about us.",
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
path: "/contact/",
|
|
595
|
+
element: (
|
|
596
|
+
<div>
|
|
597
|
+
<h1>Contact Us</h1>
|
|
598
|
+
<p>
|
|
599
|
+
Please <a href="mailto:user@example.com">send us an email</a> at
|
|
600
|
+
user@example.com
|
|
601
|
+
</p>
|
|
602
|
+
</div>
|
|
603
|
+
),
|
|
604
|
+
title: "Contact",
|
|
605
|
+
description: "Get in touch with us.",
|
|
606
|
+
},
|
|
607
|
+
]}
|
|
505
608
|
/>
|
|
506
609
|
|
|
507
610
|
<footer>
|
|
@@ -513,72 +616,49 @@ const App = () => (
|
|
|
513
616
|
</main>
|
|
514
617
|
);
|
|
515
618
|
|
|
516
|
-
|
|
619
|
+
render(<App />, document.getElementById("root"));
|
|
517
620
|
```
|
|
518
621
|
|
|
519
|
-
|
|
622
|
+
## ErrorBoundary Component
|
|
623
|
+
|
|
624
|
+
Routerino exports an `ErrorBoundary` component that you can use in your own applications to catch and handle React component errors gracefully. Fun fact: error boundary components are one of the last cases that still require using a React Class! Since this library aims to include everything you need to build a multiple page React SPA, and enable users to be able to know which component had an issue without confusing it with a Routerino bug.
|
|
520
625
|
|
|
521
|
-
|
|
626
|
+
### Import
|
|
522
627
|
|
|
523
628
|
```jsx
|
|
524
|
-
import
|
|
525
|
-
|
|
526
|
-
import Routerino from "routerino";
|
|
629
|
+
import { ErrorBoundary } from "routerino";
|
|
630
|
+
```
|
|
527
631
|
|
|
528
|
-
|
|
529
|
-
const routes = [
|
|
530
|
-
{
|
|
531
|
-
path: "/",
|
|
532
|
-
element: <p>Welcome to Home</p>,
|
|
533
|
-
title: "Home",
|
|
534
|
-
description: "Welcome to my website!",
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
path: "/about/",
|
|
538
|
-
element: <p>About us...</p>,
|
|
539
|
-
title: "About",
|
|
540
|
-
description: "Learn more about us.",
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
path: "/contact/",
|
|
544
|
-
element: (
|
|
545
|
-
<div>
|
|
546
|
-
<h1>Contact Us</h1>
|
|
547
|
-
<p>
|
|
548
|
-
Please <a href="mailto:user@example.com">send us an email</a> at
|
|
549
|
-
user@example.com
|
|
550
|
-
</p>
|
|
551
|
-
</div>
|
|
552
|
-
),
|
|
553
|
-
title: "Contact",
|
|
554
|
-
description: "Get in touch with us.",
|
|
555
|
-
},
|
|
556
|
-
];
|
|
632
|
+
### Usage
|
|
557
633
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
634
|
+
```jsx
|
|
635
|
+
<ErrorBoundary
|
|
636
|
+
fallback={<div>Something went wrong. Please try again.</div>}
|
|
637
|
+
errorTitleString="Error | My Application"
|
|
638
|
+
usePrerenderTags={true}
|
|
639
|
+
>
|
|
640
|
+
<MyComponent />
|
|
641
|
+
</ErrorBoundary>
|
|
642
|
+
```
|
|
563
643
|
|
|
564
|
-
|
|
565
|
-
{...{
|
|
566
|
-
title,
|
|
567
|
-
routes,
|
|
568
|
-
}}
|
|
569
|
-
/>
|
|
644
|
+
### Props
|
|
570
645
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
</main>
|
|
578
|
-
);
|
|
646
|
+
| Prop | Type | Required | Description |
|
|
647
|
+
| ------------------ | ----------- | -------- | ---------------------------------------------------- |
|
|
648
|
+
| `children` | `ReactNode` | No | The child components to render when there's no error |
|
|
649
|
+
| `fallback` | `ReactNode` | No | The UI to display when an error is caught |
|
|
650
|
+
| `errorTitleString` | `string` | Yes | The document title to set when an error occurs |
|
|
651
|
+
| `usePrerenderTags` | `boolean` | No | Whether to set prerender meta tag (status code 500) |
|
|
579
652
|
|
|
580
|
-
|
|
581
|
-
|
|
653
|
+
### Features
|
|
654
|
+
|
|
655
|
+
- Catches JavaScript errors in child component tree
|
|
656
|
+
- Displays fallback UI instead of white screen
|
|
657
|
+
- Sets document title on error
|
|
658
|
+
- Logs errors to console for debugging
|
|
659
|
+
- Optionally sets prerender status code for SEO
|
|
660
|
+
|
|
661
|
+
This is the same error boundary used internally by Routerino to protect your route components from crashing the entire application.
|
|
582
662
|
|
|
583
663
|
## Vendoring Routerino
|
|
584
664
|
|
|
@@ -617,7 +697,7 @@ By vendoring Routerino, you have full control over the code and can make any nec
|
|
|
617
697
|
|
|
618
698
|
## Additional Resources
|
|
619
699
|
|
|
620
|
-
|
|
700
|
+
Here are some sources for further reading on SEO best-practices.
|
|
621
701
|
|
|
622
702
|
- [Apple's best practices for link previews](https://developer.apple.com/library/archive/technotes/tn2444/_index.html)
|
|
623
703
|
- [Use Open Graph tags](https://ahrefs.com/blog/open-graph-meta-tags/)
|
package/build-static.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
// Parse command line arguments
|
|
7
|
+
const args = process.argv.slice(2).reduce((acc, arg) => {
|
|
8
|
+
const [key, value] = arg.split("=");
|
|
9
|
+
acc[key] = value;
|
|
10
|
+
return acc;
|
|
11
|
+
}, {});
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
routesFile,
|
|
15
|
+
outputDir = "./dist",
|
|
16
|
+
template = "./index.html",
|
|
17
|
+
baseUrl = "",
|
|
18
|
+
} = args;
|
|
19
|
+
|
|
20
|
+
if (!routesFile) {
|
|
21
|
+
console.error(
|
|
22
|
+
"Usage: node build-static.js routesFile=./src/routes.js outputDir=./dist template=./index.html baseUrl=https://example.com"
|
|
23
|
+
);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function buildStaticSite() {
|
|
28
|
+
try {
|
|
29
|
+
console.log("🏗️ Building static site...\n");
|
|
30
|
+
|
|
31
|
+
// Check if routes file exists
|
|
32
|
+
const routesPath = path.resolve(routesFile);
|
|
33
|
+
if (!fs.existsSync(routesPath)) {
|
|
34
|
+
throw new Error(`Routes file not found: ${routesPath}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let routes;
|
|
38
|
+
|
|
39
|
+
// Try to import as a module first (for .js/.mjs files)
|
|
40
|
+
const ext = path.extname(routesPath);
|
|
41
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
42
|
+
try {
|
|
43
|
+
const routesModule = await import(routesPath);
|
|
44
|
+
routes = routesModule.default || routesModule.routes;
|
|
45
|
+
} catch {
|
|
46
|
+
console.warn(
|
|
47
|
+
`⚠️ Could not import routes file as module, falling back to regex parsing`
|
|
48
|
+
);
|
|
49
|
+
routes = parseRoutesFromFile(routesPath);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// For JSX/TSX files, use regex parsing like build-sitemap does
|
|
53
|
+
routes = parseRoutesFromFile(routesPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!Array.isArray(routes)) {
|
|
57
|
+
throw new Error("Routes must be an array");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Read the HTML template
|
|
61
|
+
const templatePath = path.resolve(template);
|
|
62
|
+
if (!fs.existsSync(templatePath)) {
|
|
63
|
+
throw new Error(`Template file not found: ${templatePath}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const templateHtml = fs.readFileSync(templatePath, "utf-8");
|
|
67
|
+
|
|
68
|
+
// Create output directory
|
|
69
|
+
const outputPath = path.resolve(outputDir);
|
|
70
|
+
if (!fs.existsSync(outputPath)) {
|
|
71
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Process each route
|
|
75
|
+
let generatedCount = 0;
|
|
76
|
+
|
|
77
|
+
for (const route of routes) {
|
|
78
|
+
// Skip dynamic routes (with parameters)
|
|
79
|
+
if (route.path.includes(":")) {
|
|
80
|
+
console.log(`⏭️ Skipping dynamic route: ${route.path}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Generate HTML for this route
|
|
85
|
+
const html = generateHtmlForRoute(route, templateHtml, baseUrl);
|
|
86
|
+
|
|
87
|
+
// Determine output file path
|
|
88
|
+
const routePath = route.path === "/" ? "/index" : route.path;
|
|
89
|
+
const filePath = path.join(outputPath, `${routePath}.html`);
|
|
90
|
+
|
|
91
|
+
// Create directory if needed
|
|
92
|
+
const fileDir = path.dirname(filePath);
|
|
93
|
+
if (!fs.existsSync(fileDir)) {
|
|
94
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Write the HTML file
|
|
98
|
+
fs.writeFileSync(filePath, html);
|
|
99
|
+
console.log(`✅ Generated: ${filePath}`);
|
|
100
|
+
generatedCount++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Generate 404.html
|
|
104
|
+
// Create a default 404 route for proper meta tags
|
|
105
|
+
const notFoundRoute = {
|
|
106
|
+
path: "/404",
|
|
107
|
+
title: "404 - Page Not Found",
|
|
108
|
+
description: "The page you are looking for could not be found.",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Generate 404.html with proper meta tags
|
|
112
|
+
// The actual notFoundTemplate component will be rendered client-side
|
|
113
|
+
const notFoundHtml = generateHtmlForRoute(
|
|
114
|
+
notFoundRoute,
|
|
115
|
+
templateHtml,
|
|
116
|
+
baseUrl
|
|
117
|
+
);
|
|
118
|
+
const notFoundPath = path.join(outputPath, "404.html");
|
|
119
|
+
fs.writeFileSync(notFoundPath, notFoundHtml);
|
|
120
|
+
console.log(`✅ Generated: ${notFoundPath}`);
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`\n🎉 Generated ${generatedCount + 1} static HTML files in ${outputDir}`
|
|
124
|
+
);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("❌ Build failed:", error.message);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function generateHtmlForRoute(route, templateHtml, baseUrl) {
|
|
132
|
+
let html = templateHtml;
|
|
133
|
+
|
|
134
|
+
// Update title
|
|
135
|
+
if (route.title) {
|
|
136
|
+
html = html.replace(
|
|
137
|
+
/<title>.*?<\/title>/,
|
|
138
|
+
`<title>${escapeHtml(route.title)}</title>`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add/update meta tags
|
|
143
|
+
const metaTags = [];
|
|
144
|
+
|
|
145
|
+
if (route.description) {
|
|
146
|
+
metaTags.push(
|
|
147
|
+
`<meta name="description" content="${escapeHtml(route.description)}">`
|
|
148
|
+
);
|
|
149
|
+
metaTags.push(
|
|
150
|
+
`<meta property="og:description" content="${escapeHtml(route.description)}">`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (route.title) {
|
|
155
|
+
metaTags.push(
|
|
156
|
+
`<meta property="og:title" content="${escapeHtml(route.title)}">`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
metaTags.push(`<meta property="og:type" content="website">`);
|
|
161
|
+
|
|
162
|
+
if (baseUrl) {
|
|
163
|
+
metaTags.push(`<meta property="og:url" content="${baseUrl}${route.path}">`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (route.imageUrl) {
|
|
167
|
+
metaTags.push(
|
|
168
|
+
`<meta property="og:image" content="${escapeHtml(route.imageUrl)}">`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add custom tags
|
|
173
|
+
if (route.tags) {
|
|
174
|
+
route.tags.forEach((tag) => {
|
|
175
|
+
const attrs = Object.entries(tag)
|
|
176
|
+
.map(([key, value]) => `${key}="${escapeHtml(value)}"`)
|
|
177
|
+
.join(" ");
|
|
178
|
+
metaTags.push(`<meta ${attrs}>`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Insert meta tags before closing </head>
|
|
183
|
+
const metaTagsHtml = metaTags.join("\n ");
|
|
184
|
+
html = html.replace("</head>", ` ${metaTagsHtml}\n</head>`);
|
|
185
|
+
|
|
186
|
+
// Add route info as data attribute for client-side hydration
|
|
187
|
+
html = html.replace(
|
|
188
|
+
'<div id="root">',
|
|
189
|
+
`<div id="root" data-route="${escapeHtml(route.path)}">`
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return html;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function escapeHtml(str) {
|
|
196
|
+
if (typeof str !== "string") return "";
|
|
197
|
+
return str
|
|
198
|
+
.replace(/&/g, "&")
|
|
199
|
+
.replace(/</g, "<")
|
|
200
|
+
.replace(/>/g, ">")
|
|
201
|
+
.replace(/"/g, """)
|
|
202
|
+
.replace(/'/g, "'");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseRoutesFromFile(filePath) {
|
|
206
|
+
// Read file contents and strip comments
|
|
207
|
+
const fileContent = fs
|
|
208
|
+
.readFileSync(filePath, "utf-8")
|
|
209
|
+
.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "");
|
|
210
|
+
|
|
211
|
+
// Find routes array in the file
|
|
212
|
+
// Matches: routes = [...], Routes = [...], export default [...], routes: [...], etc.
|
|
213
|
+
const arrayMatch = fileContent.match(
|
|
214
|
+
/(?:[rR]outes\s*[=:]\s*\{?\s*|\bexport\s+default\s+)(\[[\s\S]*?\])/
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (!arrayMatch || !arrayMatch[1]) {
|
|
218
|
+
throw new Error("Could not find routes array in file");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const routesArrayString = arrayMatch[1];
|
|
222
|
+
|
|
223
|
+
// Parse routes from the array string
|
|
224
|
+
// This is a simplified parser that extracts route objects
|
|
225
|
+
const routes = [];
|
|
226
|
+
|
|
227
|
+
// Match route objects: { path: "...", title: "...", ... }
|
|
228
|
+
const routeMatches = routesArrayString.matchAll(/\{([^{}]*)\}/g);
|
|
229
|
+
|
|
230
|
+
for (const match of routeMatches) {
|
|
231
|
+
const routeContent = match[1];
|
|
232
|
+
const route = {};
|
|
233
|
+
|
|
234
|
+
// Extract path (required)
|
|
235
|
+
const pathMatch = routeContent.match(/path\s*:\s*(["'`])(.*?)\1/);
|
|
236
|
+
if (pathMatch) {
|
|
237
|
+
route.path = pathMatch[2];
|
|
238
|
+
} else {
|
|
239
|
+
continue; // Skip routes without paths
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Extract title
|
|
243
|
+
const titleMatch = routeContent.match(/title\s*:\s*(["'`])(.*?)\1/);
|
|
244
|
+
if (titleMatch) {
|
|
245
|
+
route.title = titleMatch[2];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Extract description
|
|
249
|
+
const descMatch = routeContent.match(/description\s*:\s*(["'`])(.*?)\1/);
|
|
250
|
+
if (descMatch) {
|
|
251
|
+
route.description = descMatch[2];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Extract imageUrl
|
|
255
|
+
const imageMatch = routeContent.match(/imageUrl\s*:\s*(["'`])(.*?)\1/);
|
|
256
|
+
if (imageMatch) {
|
|
257
|
+
route.imageUrl = imageMatch[2];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
routes.push(route);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(
|
|
264
|
+
`📝 Parsed ${routes.length} routes from ${path.basename(filePath)}`
|
|
265
|
+
);
|
|
266
|
+
return routes;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Run the build
|
|
270
|
+
buildStaticSite();
|
package/dist/routerino.js
CHANGED
|
@@ -1,62 +1,102 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { useState as
|
|
3
|
-
import
|
|
4
|
-
function
|
|
5
|
-
const
|
|
6
|
-
if (
|
|
1
|
+
import { jsx as m, jsxs as F, Fragment as H } from "react/jsx-runtime";
|
|
2
|
+
import { Component as N, useState as O, useEffect as j, cloneElement as M } from "react";
|
|
3
|
+
import t from "prop-types";
|
|
4
|
+
function n({ tag: l = "meta", soft: d = !1, ...p }) {
|
|
5
|
+
const c = Object.keys(p);
|
|
6
|
+
if (c.length < 1)
|
|
7
7
|
return console.error(
|
|
8
|
-
`updateHeadTag() received no attributes to set for ${
|
|
8
|
+
`updateHeadTag() received no attributes to set for ${l} tag`
|
|
9
9
|
);
|
|
10
|
-
let
|
|
11
|
-
for (let
|
|
12
|
-
`${
|
|
13
|
-
)), !
|
|
10
|
+
let u = null;
|
|
11
|
+
for (let s = 0; s < c.length && (c[s] !== "content" && (u = document.querySelector(
|
|
12
|
+
`${l}[${c[s]}='${p[c[s]]}']`
|
|
13
|
+
)), !u); s++)
|
|
14
14
|
;
|
|
15
|
-
!
|
|
15
|
+
!u && !d && (u = document.createElement(l)), c.forEach((s) => u.setAttribute(s, p[s])), document.querySelector("head").appendChild(u);
|
|
16
16
|
}
|
|
17
|
-
function
|
|
18
|
-
let
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
}),
|
|
17
|
+
function z({ routePattern: l, currentRoute: d }) {
|
|
18
|
+
let p = {}, c = l.split("/"), u = d.split("/");
|
|
19
|
+
return c.forEach((s, g) => {
|
|
20
|
+
s.startsWith(":") && (p[s.slice(1)] = u[g]);
|
|
21
|
+
}), p;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
class A extends N {
|
|
24
|
+
constructor(d) {
|
|
25
|
+
super(d), this.state = { hasError: !1 };
|
|
26
|
+
}
|
|
27
|
+
static getDerivedStateFromError() {
|
|
28
|
+
return { hasError: !0 };
|
|
29
|
+
}
|
|
30
|
+
componentDidCatch(d, p) {
|
|
31
|
+
console.group("🚨 Routerino Error Boundary Caught an Error"), console.error("Error:", d), console.error("Component Stack:", p.componentStack), this.props.routePath && console.error("Failed Route:", this.props.routePath), console.error("Error occurred at:", (/* @__PURE__ */ new Date()).toISOString()), console.groupEnd(), document.title = this.props.errorTitleString, this.props.usePrerenderTags && n({ name: "prerender-status-code", content: "500" });
|
|
32
|
+
}
|
|
33
|
+
render() {
|
|
34
|
+
return this.state.hasError ? this.props.fallback : this.props.children;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
A.propTypes = {
|
|
38
|
+
/** The child components to render when there's no error */
|
|
39
|
+
children: t.node,
|
|
40
|
+
/** The fallback UI to display when an error is caught */
|
|
41
|
+
fallback: t.node,
|
|
42
|
+
/** The document title to set when an error occurs */
|
|
43
|
+
errorTitleString: t.string.isRequired,
|
|
44
|
+
/** Whether to set prerender meta tags (status code 500) on error */
|
|
45
|
+
usePrerenderTags: t.bool,
|
|
46
|
+
/** The current route path for better error context (optional) */
|
|
47
|
+
routePath: t.string
|
|
48
|
+
};
|
|
49
|
+
function I({
|
|
50
|
+
routes: l = [
|
|
25
51
|
{
|
|
26
52
|
path: "/",
|
|
27
|
-
element: /* @__PURE__ */
|
|
53
|
+
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." }),
|
|
28
54
|
title: "Routerino default route example",
|
|
29
55
|
description: "The default route example description.",
|
|
30
56
|
tags: [{ property: "og:locale", content: "en_US" }]
|
|
31
57
|
}
|
|
32
58
|
],
|
|
33
|
-
notFoundTemplate:
|
|
34
|
-
/* @__PURE__ */
|
|
35
|
-
/* @__PURE__ */
|
|
59
|
+
notFoundTemplate: d = /* @__PURE__ */ F(H, { children: [
|
|
60
|
+
/* @__PURE__ */ m("p", { children: "No page found for this URL. [404]" }),
|
|
61
|
+
/* @__PURE__ */ m("p", { children: /* @__PURE__ */ m("a", { href: "/", children: "Home" }) })
|
|
36
62
|
] }),
|
|
37
|
-
notFoundTitle:
|
|
38
|
-
errorTemplate:
|
|
39
|
-
/* @__PURE__ */
|
|
40
|
-
/* @__PURE__ */
|
|
63
|
+
notFoundTitle: p = "Page not found [404]",
|
|
64
|
+
errorTemplate: c = /* @__PURE__ */ F(H, { children: [
|
|
65
|
+
/* @__PURE__ */ m("p", { children: "Page failed to load. [500]" }),
|
|
66
|
+
/* @__PURE__ */ m("p", { children: /* @__PURE__ */ m("a", { href: "/", children: "Home" }) })
|
|
41
67
|
] }),
|
|
42
|
-
errorTitle:
|
|
43
|
-
useTrailingSlash:
|
|
68
|
+
errorTitle: u = "Page error [500]",
|
|
69
|
+
useTrailingSlash: s = !0,
|
|
44
70
|
usePrerenderTags: g = !0,
|
|
45
|
-
title:
|
|
46
|
-
separator:
|
|
47
|
-
titlePrefix:
|
|
48
|
-
titlePostfix:
|
|
49
|
-
imageUrl:
|
|
71
|
+
title: E = "",
|
|
72
|
+
separator: S = " | ",
|
|
73
|
+
titlePrefix: y = "",
|
|
74
|
+
titlePostfix: $ = "",
|
|
75
|
+
imageUrl: b = null,
|
|
50
76
|
touchIconUrl: P = null,
|
|
51
77
|
debug: f = !1
|
|
52
78
|
}) {
|
|
53
|
-
var
|
|
79
|
+
var v, x, U;
|
|
80
|
+
const T = `${y}${u}${$ || `${S}${E}`}`, k = `${y}${p}${$ || `${S}${E}`}`;
|
|
54
81
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
if (f || window.location.host === "localhost" || window.location.host.includes("localhost:")) {
|
|
83
|
+
y !== "" && console.warn(
|
|
84
|
+
"Routerino: titlePrefix is deprecated and will be removed in v2.0. Please migrate to the title and separator props instead."
|
|
85
|
+
), $ !== "" && console.warn(
|
|
86
|
+
"Routerino: titlePostfix is deprecated and will be removed in v2.0. Please migrate to the title and separator props instead."
|
|
87
|
+
);
|
|
88
|
+
const e = l.map((h) => h.path), i = e.filter(
|
|
89
|
+
(h, a) => e.indexOf(h) !== a
|
|
90
|
+
);
|
|
91
|
+
i.length > 0 && (console.warn("⚠️ Routerino: Duplicate route paths detected:", [
|
|
92
|
+
...new Set(i)
|
|
93
|
+
]), console.warn("The first matching route will be used"));
|
|
94
|
+
}
|
|
95
|
+
const [R, q] = O(window.location.href);
|
|
96
|
+
j(() => {
|
|
97
|
+
const e = (h) => {
|
|
58
98
|
f && console.debug("click occurred");
|
|
59
|
-
let a =
|
|
99
|
+
let a = h.target;
|
|
60
100
|
for (; a.tagName !== "A" && a.parentElement; )
|
|
61
101
|
a = a.parentElement;
|
|
62
102
|
if (a.tagName !== "A") {
|
|
@@ -64,104 +104,136 @@ function O({
|
|
|
64
104
|
return;
|
|
65
105
|
}
|
|
66
106
|
f && console.debug(`click target ${a}`);
|
|
67
|
-
let
|
|
68
|
-
f && console.debug(`targetUrl: ${
|
|
107
|
+
let w = new URL(a);
|
|
108
|
+
f && console.debug(`targetUrl: ${w}, current: ${window.location}`), window.location.origin === w.origin ? (f && console.debug(
|
|
69
109
|
"target link is same origin, push-state transitioning"
|
|
70
|
-
),
|
|
110
|
+
), h.preventDefault(), a.href !== window.location.href && (q(a.href), window.history.pushState({}, "", a.href)), window.scrollTo({
|
|
71
111
|
top: 0,
|
|
72
112
|
behavior: "auto"
|
|
73
113
|
})) : f && console.debug(
|
|
74
114
|
"target link does not share an origin, standard link handling applies"
|
|
75
115
|
);
|
|
76
116
|
};
|
|
77
|
-
document.addEventListener("click",
|
|
78
|
-
const
|
|
79
|
-
|
|
117
|
+
document.addEventListener("click", e);
|
|
118
|
+
const i = () => {
|
|
119
|
+
q(window.location.href);
|
|
80
120
|
};
|
|
81
|
-
return window.addEventListener("popstate",
|
|
82
|
-
document.removeEventListener("click",
|
|
121
|
+
return window.addEventListener("popstate", i), () => {
|
|
122
|
+
document.removeEventListener("click", e), window.removeEventListener("popstate", i);
|
|
83
123
|
};
|
|
84
|
-
}, [
|
|
85
|
-
let
|
|
86
|
-
(
|
|
87
|
-
const
|
|
88
|
-
(
|
|
89
|
-
),
|
|
90
|
-
const
|
|
91
|
-
return a.length !==
|
|
92
|
-
}),
|
|
93
|
-
if (f && console.debug({ match:
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
document.title =
|
|
124
|
+
}, [R]);
|
|
125
|
+
let r = ((v = window.location) == null ? void 0 : v.pathname) ?? "/";
|
|
126
|
+
(r === "/index.html" || r === "") && (r = "/");
|
|
127
|
+
const B = l.find((e) => e.path === r), L = l.find(
|
|
128
|
+
(e) => `${e.path}/` === r || e.path === `${r}/`
|
|
129
|
+
), C = l.find((e) => {
|
|
130
|
+
const i = e.path.endsWith("/") ? e.path.slice(0, -1) : e.path, h = r.endsWith("/") ? r.slice(0, -1) : r, a = i.split("/").filter(Boolean), w = h.split("/").filter(Boolean);
|
|
131
|
+
return a.length !== w.length ? !1 : a.every((W, D) => W.startsWith(":") ? !0 : W === w[D]);
|
|
132
|
+
}), o = B ?? L ?? C;
|
|
133
|
+
if (f && console.debug({ match: o, exactMatch: B, addSlashMatch: L, paramsMatch: C }), !o)
|
|
134
|
+
return (f || window.location.host === "localhost" || window.location.host.includes("localhost:")) && (console.group("⚠️ Routerino 404 - No matching route"), console.warn(`Requested path: ${r}`), console.warn(
|
|
135
|
+
"Available routes:",
|
|
136
|
+
l.map((e) => e.path)
|
|
137
|
+
), console.groupEnd()), document.title = k, g && n({ name: "prerender-status-code", content: "404" }), d;
|
|
138
|
+
if (g) {
|
|
139
|
+
const e = document.querySelector(
|
|
140
|
+
'meta[name="prerender-status-code"]'
|
|
141
|
+
);
|
|
142
|
+
e && e.remove();
|
|
143
|
+
const i = document.querySelector(
|
|
144
|
+
'meta[name="prerender-header"]'
|
|
145
|
+
);
|
|
146
|
+
i && i.remove();
|
|
147
|
+
}
|
|
148
|
+
if (o.title) {
|
|
149
|
+
const e = `${o.titlePrefix ?? y}${o.title}${o.titlePostfix || $ || `${S}${E}`}`;
|
|
150
|
+
document.title = e, (x = o.tags) != null && x.find(({ property: i }) => i === "og:title") || n({
|
|
98
151
|
property: "og:title",
|
|
99
|
-
content:
|
|
152
|
+
content: e
|
|
100
153
|
});
|
|
101
154
|
}
|
|
102
|
-
if (
|
|
155
|
+
if (o.description && (n({ name: "description", content: o.description }), (U = o.tags) != null && U.find(({ property: e }) => e === "og:description") || n({
|
|
103
156
|
property: "og:description",
|
|
104
|
-
content:
|
|
105
|
-
})), (
|
|
157
|
+
content: o.description
|
|
158
|
+
})), (b || o.imageUrl) && n({
|
|
106
159
|
property: "og:image",
|
|
107
|
-
content:
|
|
108
|
-
}), P &&
|
|
160
|
+
content: o.imageUrl ?? b
|
|
161
|
+
}), P && n({
|
|
109
162
|
tag: "link",
|
|
110
163
|
rel: "apple-touch-icon",
|
|
111
164
|
href: P
|
|
112
|
-
}), g &&
|
|
165
|
+
}), g && r !== "/" && (s && !r.endsWith("/") ? (n({ name: "prerender-status-code", content: "301" }), n({
|
|
113
166
|
name: "prerender-header",
|
|
114
167
|
content: `Location: ${window.location.href}/`
|
|
115
|
-
})) : !
|
|
168
|
+
})) : !s && r.endsWith("/") && (n({ name: "prerender-status-code", content: "301" }), n({
|
|
116
169
|
name: "prerender-header",
|
|
117
170
|
content: `Location: ${window.location.href.slice(0, -1)}`
|
|
118
|
-
}))),
|
|
119
|
-
const
|
|
120
|
-
routePattern:
|
|
121
|
-
currentRoute:
|
|
122
|
-
}),
|
|
123
|
-
currentRoute:
|
|
124
|
-
params:
|
|
125
|
-
routePattern:
|
|
126
|
-
updateHeadTag:
|
|
127
|
-
}
|
|
128
|
-
return C(n.element, {
|
|
171
|
+
}))), o.tags && o.tags.length ? (o.tags.find(({ property: e }) => e === "og:type") || n({ property: "og:type", content: "website" }), o.tags.forEach((e) => n(e))) : n({ property: "og:type", content: "website" }), o.element) {
|
|
172
|
+
const e = z({
|
|
173
|
+
routePattern: o.path,
|
|
174
|
+
currentRoute: r
|
|
175
|
+
}), i = {
|
|
176
|
+
currentRoute: r,
|
|
177
|
+
params: e,
|
|
178
|
+
routePattern: o.path,
|
|
179
|
+
updateHeadTag: n
|
|
180
|
+
}, h = M(o.element, {
|
|
129
181
|
// we allow access via both uppercase and lowercase
|
|
130
|
-
routerino:
|
|
131
|
-
Routerino:
|
|
182
|
+
routerino: i,
|
|
183
|
+
Routerino: i
|
|
132
184
|
});
|
|
185
|
+
return /* @__PURE__ */ m(
|
|
186
|
+
A,
|
|
187
|
+
{
|
|
188
|
+
fallback: c,
|
|
189
|
+
errorTitleString: T,
|
|
190
|
+
usePrerenderTags: g,
|
|
191
|
+
routePath: r,
|
|
192
|
+
children: h
|
|
193
|
+
}
|
|
194
|
+
);
|
|
133
195
|
}
|
|
134
|
-
return console.error(`No route found for ${
|
|
135
|
-
} catch (
|
|
136
|
-
return console.
|
|
196
|
+
return console.error(`No route found for ${r}`), document.title = k, g && n({ name: "prerender-status-code", content: "404" }), d;
|
|
197
|
+
} catch (R) {
|
|
198
|
+
return console.group("💥 Routerino Fatal Error"), console.error(
|
|
199
|
+
"An error occurred in the router itself (not in a route component)"
|
|
200
|
+
), console.error("Error:", R), console.error(
|
|
201
|
+
"This typically means an issue with route configuration or router setup"
|
|
202
|
+
), console.groupEnd(), g && n({ name: "prerender-status-code", content: "500" }), document.title = T, c;
|
|
137
203
|
}
|
|
138
204
|
}
|
|
139
|
-
const
|
|
140
|
-
path:
|
|
141
|
-
element:
|
|
142
|
-
title:
|
|
143
|
-
description:
|
|
144
|
-
tags:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
205
|
+
const K = t.exact({
|
|
206
|
+
path: t.string.isRequired,
|
|
207
|
+
element: t.element.isRequired,
|
|
208
|
+
title: t.string,
|
|
209
|
+
description: t.string,
|
|
210
|
+
tags: t.arrayOf(t.object),
|
|
211
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
212
|
+
titlePrefix: t.string,
|
|
213
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
214
|
+
titlePostfix: t.string,
|
|
215
|
+
imageUrl: t.string
|
|
148
216
|
});
|
|
149
|
-
|
|
150
|
-
routes:
|
|
151
|
-
title:
|
|
152
|
-
separator:
|
|
153
|
-
notFoundTemplate:
|
|
154
|
-
notFoundTitle:
|
|
155
|
-
errorTemplate:
|
|
156
|
-
errorTitle:
|
|
157
|
-
useTrailingSlash:
|
|
158
|
-
usePrerenderTags:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
217
|
+
I.propTypes = {
|
|
218
|
+
routes: t.arrayOf(K),
|
|
219
|
+
title: t.string,
|
|
220
|
+
separator: t.string,
|
|
221
|
+
notFoundTemplate: t.element,
|
|
222
|
+
notFoundTitle: t.string,
|
|
223
|
+
errorTemplate: t.element,
|
|
224
|
+
errorTitle: t.string,
|
|
225
|
+
useTrailingSlash: t.bool,
|
|
226
|
+
usePrerenderTags: t.bool,
|
|
227
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
228
|
+
titlePrefix: t.string,
|
|
229
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
230
|
+
titlePostfix: t.string,
|
|
231
|
+
imageUrl: t.string,
|
|
232
|
+
touchIconUrl: t.string,
|
|
233
|
+
debug: t.bool
|
|
164
234
|
};
|
|
165
235
|
export {
|
|
166
|
-
|
|
236
|
+
A as ErrorBoundary,
|
|
237
|
+
I as default,
|
|
238
|
+
n as updateHeadTag
|
|
167
239
|
};
|
package/dist/routerino.umd.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(d,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],i):(d=typeof globalThis<"u"?globalThis:d||self,i(d.routerino={},d["react/jsx-runtime"],d.React,d.PropTypes))})(this,function(d,i,$,t){"use strict";function r({tag:s="meta",soft:h=!1,...f}){const u=Object.keys(f);if(u.length<1)return console.error(`updateHeadTag() received no attributes to set for ${s} tag`);let p=null;for(let c=0;c<u.length&&(u[c]!=="content"&&(p=document.querySelector(`${s}[${u[c]}='${f[u[c]]}']`)),!p);c++);!p&&!h&&(p=document.createElement(s)),u.forEach(c=>p.setAttribute(c,f[c])),document.querySelector("head").appendChild(p)}function M({routePattern:s,currentRoute:h}){let f={},u=s.split("/"),p=h.split("/");return u.forEach((c,w)=>{c.startsWith(":")&&(f[c.slice(1)]=p[w])}),f}class v extends $.Component{constructor(h){super(h),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(h,f){console.group("🚨 Routerino Error Boundary Caught an Error"),console.error("Error:",h),console.error("Component Stack:",f.componentStack),this.props.routePath&&console.error("Failed Route:",this.props.routePath),console.error("Error occurred at:",new Date().toISOString()),console.groupEnd(),document.title=this.props.errorTitleString,this.props.usePrerenderTags&&r({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}v.propTypes={children:t.node,fallback:t.node,errorTitleString:t.string.isRequired,usePrerenderTags:t.bool,routePath:t.string};function q({routes:s=[{path:"/",element:i.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:h=i.jsxs(i.Fragment,{children:[i.jsx("p",{children:"No page found for this URL. [404]"}),i.jsx("p",{children:i.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:f="Page not found [404]",errorTemplate:u=i.jsxs(i.Fragment,{children:[i.jsx("p",{children:"Page failed to load. [500]"}),i.jsx("p",{children:i.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:p="Page error [500]",useTrailingSlash:c=!0,usePrerenderTags:w=!0,title:k="",separator:R=" | ",titlePrefix:S="",titlePostfix:b="",imageUrl:B=null,touchIconUrl:U=null,debug:m=!1}){var C,F,H;const y=`${S}${p}${b||`${R}${k}`}`,L=`${S}${f}${b||`${R}${k}`}`;try{if(m||window.location.host==="localhost"||window.location.host.includes("localhost:")){S!==""&&console.warn("Routerino: titlePrefix is deprecated and will be removed in v2.0. Please migrate to the title and separator props instead."),b!==""&&console.warn("Routerino: titlePostfix is deprecated and will be removed in v2.0. Please migrate to the title and separator props instead.");const e=s.map(g=>g.path),a=e.filter((g,l)=>e.indexOf(g)!==l);a.length>0&&(console.warn("⚠️ Routerino: Duplicate route paths detected:",[...new Set(a)]),console.warn("The first matching route will be used"))}const[x,W]=$.useState(window.location.href);$.useEffect(()=>{const e=g=>{m&&console.debug("click occurred");let l=g.target;for(;l.tagName!=="A"&&l.parentElement;)l=l.parentElement;if(l.tagName!=="A"){m&&console.debug("no achor tag found during click");return}m&&console.debug(`click target ${l}`);let E=new URL(l);m&&console.debug(`targetUrl: ${E}, current: ${window.location}`),window.location.origin===E.origin?(m&&console.debug("target link is same origin, push-state transitioning"),g.preventDefault(),l.href!==window.location.href&&(W(l.href),window.history.pushState({},"",l.href)),window.scrollTo({top:0,behavior:"auto"})):m&&console.debug("target link does not share an origin, standard link handling applies")};document.addEventListener("click",e);const a=()=>{W(window.location.href)};return window.addEventListener("popstate",a),()=>{document.removeEventListener("click",e),window.removeEventListener("popstate",a)}},[x]);let n=((C=window.location)==null?void 0:C.pathname)??"/";(n==="/index.html"||n==="")&&(n="/");const j=s.find(e=>e.path===n),O=s.find(e=>`${e.path}/`===n||e.path===`${n}/`),A=s.find(e=>{const a=e.path.endsWith("/")?e.path.slice(0,-1):e.path,g=n.endsWith("/")?n.slice(0,-1):n,l=a.split("/").filter(Boolean),E=g.split("/").filter(Boolean);return l.length!==E.length?!1:l.every((D,_)=>D.startsWith(":")?!0:D===E[_])}),o=j??O??A;if(m&&console.debug({match:o,exactMatch:j,addSlashMatch:O,paramsMatch:A}),!o)return(m||window.location.host==="localhost"||window.location.host.includes("localhost:"))&&(console.group("⚠️ Routerino 404 - No matching route"),console.warn(`Requested path: ${n}`),console.warn("Available routes:",s.map(e=>e.path)),console.groupEnd()),document.title=L,w&&r({name:"prerender-status-code",content:"404"}),h;if(w){const e=document.querySelector('meta[name="prerender-status-code"]');e&&e.remove();const a=document.querySelector('meta[name="prerender-header"]');a&&a.remove()}if(o.title){const e=`${o.titlePrefix??S}${o.title}${o.titlePostfix||b||`${R}${k}`}`;document.title=e,(F=o.tags)!=null&&F.find(({property:a})=>a==="og:title")||r({property:"og:title",content:e})}if(o.description&&(r({name:"description",content:o.description}),(H=o.tags)!=null&&H.find(({property:e})=>e==="og:description")||r({property:"og:description",content:o.description})),(B||o.imageUrl)&&r({property:"og:image",content:o.imageUrl??B}),U&&r({tag:"link",rel:"apple-touch-icon",href:U}),w&&n!=="/"&&(c&&!n.endsWith("/")?(r({name:"prerender-status-code",content:"301"}),r({name:"prerender-header",content:`Location: ${window.location.href}/`})):!c&&n.endsWith("/")&&(r({name:"prerender-status-code",content:"301"}),r({name:"prerender-header",content:`Location: ${window.location.href.slice(0,-1)}`}))),o.tags&&o.tags.length?(o.tags.find(({property:e})=>e==="og:type")||r({property:"og:type",content:"website"}),o.tags.forEach(e=>r(e))):r({property:"og:type",content:"website"}),o.element){const e=M({routePattern:o.path,currentRoute:n}),a={currentRoute:n,params:e,routePattern:o.path,updateHeadTag:r},g=$.cloneElement(o.element,{routerino:a,Routerino:a});return i.jsx(v,{fallback:u,errorTitleString:y,usePrerenderTags:w,routePath:n,children:g})}return console.error(`No route found for ${n}`),document.title=L,w&&r({name:"prerender-status-code",content:"404"}),h}catch(x){return console.group("💥 Routerino Fatal Error"),console.error("An error occurred in the router itself (not in a route component)"),console.error("Error:",x),console.error("This typically means an issue with route configuration or router setup"),console.groupEnd(),w&&r({name:"prerender-status-code",content:"500"}),document.title=y,u}}const N=t.exact({path:t.string.isRequired,element:t.element.isRequired,title:t.string,description:t.string,tags:t.arrayOf(t.object),titlePrefix:t.string,titlePostfix:t.string,imageUrl:t.string});q.propTypes={routes:t.arrayOf(N),title:t.string,separator:t.string,notFoundTemplate:t.element,notFoundTitle:t.string,errorTemplate:t.element,errorTitle:t.string,useTrailingSlash:t.bool,usePrerenderTags:t.bool,titlePrefix:t.string,titlePostfix:t.string,imageUrl:t.string,touchIconUrl:t.string,debug:t.bool},d.ErrorBoundary=v,d.default=q,d.updateHeadTag=r,Object.defineProperties(d,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routerino",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A lightweight, SEO-optimized React router for modern web applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
"homepage": "https://github.com/nerds-with-keyboards/routerino#readme",
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
-
"types"
|
|
23
|
+
"types",
|
|
24
|
+
"build-sitemap.js",
|
|
25
|
+
"build-static.js",
|
|
26
|
+
"prerender",
|
|
27
|
+
"docs/deployment"
|
|
24
28
|
],
|
|
25
29
|
"bin": {
|
|
26
30
|
"routerino-build-sitemap": "build-sitemap.js",
|
|
27
|
-
"build-
|
|
31
|
+
"routerino-build-static": "build-static.js"
|
|
28
32
|
},
|
|
29
33
|
"type": "module",
|
|
30
34
|
"types": "types/routerino.d.ts",
|
|
@@ -36,31 +40,66 @@
|
|
|
36
40
|
},
|
|
37
41
|
"scripts": {
|
|
38
42
|
"build": "vite build",
|
|
39
|
-
"
|
|
43
|
+
"build:static": "node build-static.js",
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
46
|
+
"test:react-versions": "node test-react-versions.js",
|
|
47
|
+
"test:node-versions": "node test-node-versions.js",
|
|
40
48
|
"prepublishOnly": "vite build",
|
|
41
|
-
"lint": "eslint --fix . --max-warnings=0"
|
|
49
|
+
"lint": "eslint --fix . --max-warnings=0",
|
|
50
|
+
"lint:fix": "eslint --fix .",
|
|
51
|
+
"fix:entities": "find demo-prerender demo-static -name '*.jsx' -exec sed -i '' 's/\"/{\\\"}/g; s/'\"'\"'/{\\\"'\"'\"'}/g' {} +",
|
|
52
|
+
"format": "prettier --write .",
|
|
53
|
+
"format:check": "prettier --check .",
|
|
54
|
+
"prepare": "husky"
|
|
42
55
|
},
|
|
43
56
|
"devDependencies": {
|
|
44
57
|
"@eslint/js": "^9.27.0",
|
|
58
|
+
"@testing-library/react": "^16.3.0",
|
|
59
|
+
"@testing-library/user-event": "^14.6.1",
|
|
45
60
|
"@vitejs/plugin-react": "^4.5.0",
|
|
46
61
|
"eslint": "^9.27.0",
|
|
47
62
|
"eslint-plugin-react": "^7.37.5",
|
|
63
|
+
"express": "^4.18.2",
|
|
48
64
|
"globals": "^16.1.0",
|
|
65
|
+
"husky": "^9.1.7",
|
|
66
|
+
"jsdom": "^26.1.0",
|
|
67
|
+
"lint-staged": "^16.1.2",
|
|
68
|
+
"node-fetch": "^3.3.2",
|
|
69
|
+
"prettier": "^3.6.2",
|
|
49
70
|
"prop-types": "^15.8.1",
|
|
50
71
|
"react": "^19.1.0",
|
|
51
72
|
"react-dom": "^19.1.0",
|
|
52
|
-
"vite": "^6.3.5"
|
|
73
|
+
"vite": "^6.3.5",
|
|
74
|
+
"vitest": "^3.2.4"
|
|
53
75
|
},
|
|
54
76
|
"peerDependencies": {
|
|
55
77
|
"prop-types": "^15.0.0",
|
|
56
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
57
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
78
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^10.0.0",
|
|
79
|
+
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^10.0.0"
|
|
80
|
+
},
|
|
81
|
+
"peerDependenciesMeta": {
|
|
82
|
+
"react": {
|
|
83
|
+
"optional": false
|
|
84
|
+
},
|
|
85
|
+
"react-dom": {
|
|
86
|
+
"optional": false
|
|
87
|
+
}
|
|
58
88
|
},
|
|
59
89
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
90
|
+
"node": ">=18"
|
|
61
91
|
},
|
|
62
92
|
"volta": {
|
|
63
93
|
"node": "22.16.0",
|
|
64
94
|
"npm": "10.9.2"
|
|
95
|
+
},
|
|
96
|
+
"lint-staged": {
|
|
97
|
+
"*.{js,jsx,mjs,cjs}": [
|
|
98
|
+
"eslint --fix --max-warnings=0 --no-warn-ignored",
|
|
99
|
+
"prettier --write"
|
|
100
|
+
],
|
|
101
|
+
"*.{json,md,yml,yaml,css,html}": [
|
|
102
|
+
"prettier --write"
|
|
103
|
+
]
|
|
65
104
|
}
|
|
66
105
|
}
|
package/types/routerino.d.ts
CHANGED
|
@@ -26,7 +26,9 @@ export interface RouteConfig {
|
|
|
26
26
|
title?: string;
|
|
27
27
|
description?: string;
|
|
28
28
|
tags?: HeadTag[];
|
|
29
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
29
30
|
titlePrefix?: string;
|
|
31
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
30
32
|
titlePostfix?: string;
|
|
31
33
|
imageUrl?: string;
|
|
32
34
|
}
|
|
@@ -41,13 +43,42 @@ export interface RouterinoProps {
|
|
|
41
43
|
useTrailingSlash?: boolean;
|
|
42
44
|
usePrerenderTags?: boolean;
|
|
43
45
|
separator?: string;
|
|
46
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
44
47
|
titlePrefix?: string;
|
|
48
|
+
/** @deprecated Use title and separator props instead. Will be removed in v2.0 */
|
|
45
49
|
titlePostfix?: string;
|
|
46
50
|
imageUrl?: string;
|
|
47
51
|
touchIconUrl?: string;
|
|
48
52
|
debug?: boolean;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
export interface ErrorBoundaryProps {
|
|
56
|
+
/** The child components to render when there's no error */
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
/** The fallback UI to display when an error is caught */
|
|
59
|
+
fallback?: React.ReactNode;
|
|
60
|
+
/** The document title to set when an error occurs */
|
|
61
|
+
errorTitleString: string;
|
|
62
|
+
/** Whether to set prerender meta tags (status code 500) on error */
|
|
63
|
+
usePrerenderTags?: boolean;
|
|
64
|
+
/** The current route path for better error context (optional) */
|
|
65
|
+
routePath?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ErrorBoundaryState {
|
|
69
|
+
hasError: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class ErrorBoundary extends React.Component<
|
|
73
|
+
ErrorBoundaryProps,
|
|
74
|
+
ErrorBoundaryState
|
|
75
|
+
> {
|
|
76
|
+
constructor(props: ErrorBoundaryProps);
|
|
77
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
78
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
|
|
79
|
+
render(): React.ReactNode;
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
declare function Routerino(props: RouterinoProps): JSX.Element;
|
|
52
83
|
|
|
53
84
|
export default Routerino;
|