pjdev2d-cli 1.1.3 → 1.2.1
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 +6 -0
- package/package.json +2 -1
- package/registry.json +5 -0
- package/templates/react-router/hook/useRouterSearch.ts +131 -0
- package/templates/react-router/router/guards/private.route.tsx +56 -0
- package/templates/react-router/router/guards/public.route.tsx +21 -0
- package/templates/react-router/router/index.ts +186 -0
- package/templates/react-router/router/layouts/auth.layout.tsx +5 -0
- package/templates/react-router/router/layouts/error.layout.tsx +15 -0
- package/templates/react-router/router/layouts/main.layout.tsx +5 -0
- package/templates/react-router/router/path.ts +7 -0
- package/templates/react-router/routes/not-found/index.tsx +3 -0
- package/templates/react-router/routes/page-auth/index.tsx +3 -0
- package/templates/react-router/routes/page-one/index.tsx +85 -0
- package/templates/react-router/routes/page-one/search.page-one.ts +5 -0
- package/templates/react-router/routes/page-root/index.tsx +3 -0
- package/templates/react-router/routes/page-two/index.tsx +3 -0
- package/templates/react-router/routes/page-two/search.page-two.ts +5 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pjdev2d-cli",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI to install reusable React components like shadcn/ui",
|
|
6
6
|
"main": "bin/cli.js",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"fs-extra": "^11.3.5",
|
|
31
31
|
"inquirer": "^13.4.3",
|
|
32
32
|
"react": "^19.2.6",
|
|
33
|
+
"react-router-dom": "^7.16.0",
|
|
33
34
|
"tailwind-merge": "^3.6.0"
|
|
34
35
|
}
|
|
35
36
|
}
|
package/registry.json
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export function useRouterSearch<T extends Record<string, any>>({
|
|
5
|
+
defaultParams,
|
|
6
|
+
}: {
|
|
7
|
+
defaultParams: T;
|
|
8
|
+
}) {
|
|
9
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
10
|
+
|
|
11
|
+
const defaultRef = useRef(defaultParams);
|
|
12
|
+
defaultRef.current = defaultParams;
|
|
13
|
+
|
|
14
|
+
const search = useMemo(() => {
|
|
15
|
+
const result = { ...defaultRef.current };
|
|
16
|
+
const keys = Object.keys(defaultRef.current);
|
|
17
|
+
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
const fallback = defaultRef.current[key];
|
|
20
|
+
if (Array.isArray(fallback)) {
|
|
21
|
+
const val = searchParams.getAll(key);
|
|
22
|
+
if (val.length > 0) {
|
|
23
|
+
result[key as keyof T] = parseValue(val, fallback) as any;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const val = searchParams.get(key);
|
|
27
|
+
if (val !== null) {
|
|
28
|
+
result[key as keyof T] = parseValue(val, fallback) as any;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}, [searchParams]);
|
|
34
|
+
|
|
35
|
+
const setSearch = useCallback(
|
|
36
|
+
(
|
|
37
|
+
updates: Partial<Record<keyof T, any>>,
|
|
38
|
+
options?: { resetPage?: boolean; replace?: boolean }
|
|
39
|
+
) => {
|
|
40
|
+
const next = new URLSearchParams(searchParams);
|
|
41
|
+
const keys = Object.keys(defaultRef.current);
|
|
42
|
+
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
const value = updates[key as keyof T];
|
|
45
|
+
if (value !== undefined) {
|
|
46
|
+
if (value === null || areEqual(value, defaultRef.current[key])) {
|
|
47
|
+
next.delete(key);
|
|
48
|
+
} else if (Array.isArray(value)) {
|
|
49
|
+
next.delete(key);
|
|
50
|
+
value.forEach((val) => {
|
|
51
|
+
if (val !== undefined && val !== null && val !== "") {
|
|
52
|
+
next.append(key, String(val));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
next.set(key, String(value));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reset pagination
|
|
62
|
+
* when filters/search changes
|
|
63
|
+
*/
|
|
64
|
+
if (options?.resetPage && "page" in defaultRef.current) {
|
|
65
|
+
next.delete("page");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setSearchParams(next, {
|
|
69
|
+
replace: options?.replace,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
[searchParams, setSearchParams]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return { search, setSearch };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function areEqual(a: any, b: any) {
|
|
79
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
80
|
+
if (a.length !== b.length) return false;
|
|
81
|
+
const sortedA = [...a].sort();
|
|
82
|
+
const sortedB = [...b].sort();
|
|
83
|
+
return sortedA.every((val, index) => val === sortedB[index]);
|
|
84
|
+
}
|
|
85
|
+
return a === b;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseValue(value: string | string[], fallback: any): any {
|
|
89
|
+
if (Array.isArray(fallback)) {
|
|
90
|
+
const fallbackVal = fallback[0];
|
|
91
|
+
const valArray = Array.isArray(value) ? value : [value];
|
|
92
|
+
return valArray.map((item) => parseSingleValue(item, fallbackVal));
|
|
93
|
+
}
|
|
94
|
+
const singleVal = Array.isArray(value) ? value[0] : value;
|
|
95
|
+
return parseSingleValue(singleVal, fallback);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseSingleValue(value: string, fallback: any) {
|
|
99
|
+
if (typeof fallback === "number") {
|
|
100
|
+
const parsed = Number(value);
|
|
101
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Boolean parsing
|
|
106
|
+
*/
|
|
107
|
+
if (typeof fallback === "boolean") {
|
|
108
|
+
return value === "true";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* String fallback
|
|
113
|
+
*/
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
# NOTE: Array parameter usage example
|
|
119
|
+
|
|
120
|
+
1. Define the default parameter as an array:
|
|
121
|
+
export const DefaultSearch = {
|
|
122
|
+
color: [] as string[],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
2. Set values (e.g., in a component):
|
|
126
|
+
setSearch({ color: ["red", "blue"] });
|
|
127
|
+
// Resulting URL query: ?color=red&color=blue
|
|
128
|
+
|
|
129
|
+
3. Read values (e.g., in a component):
|
|
130
|
+
const colors = search.color; // Returns ["red", "blue"]
|
|
131
|
+
*/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export function PrivateRoute() {
|
|
4
|
+
return <Outlet />;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
# NOTE : use this when you want to redirect user to login page if he is already logged out.
|
|
9
|
+
# Includes the premium "Redirect Back" pattern to preserve user navigation state.
|
|
10
|
+
|
|
11
|
+
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
|
12
|
+
|
|
13
|
+
export default function PrivateRoute() {
|
|
14
|
+
const token = getLocalAuthToken();
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
|
|
17
|
+
if (!token) {
|
|
18
|
+
// Passes the requested path inside search params so you can redirect back after successful login
|
|
19
|
+
return (
|
|
20
|
+
<Navigate
|
|
21
|
+
to={`/page_auth?redirectTo=${encodeURIComponent(location.pathname + location.search)}`}
|
|
22
|
+
replace
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return <Outlet />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# NOTE: HOW THE LOGIN COMPONENT CONSUMES THE "redirectTo" PARAMETER
|
|
30
|
+
|
|
31
|
+
### The Bookmarking Lifecycle Flow:
|
|
32
|
+
1. **Bookmarked Access:** A logged-out user tries to access a bookmarked private route directly:
|
|
33
|
+
`https://your-app.com/page_two`
|
|
34
|
+
2. **Redirect to Auth:** The `PrivateRoute` guard intercepts the request, captures the target pathname (`/page_two`), and redirects them to the login screen with the target appended:
|
|
35
|
+
`https://your-app.com/page_auth?redirectTo=%2Fpage_two`
|
|
36
|
+
3. **Login:** The user submits their login credentials.
|
|
37
|
+
4. **Target Restoration:** Upon successful authentication, the Login page checks the URL for `redirectTo` and navigates the user straight to `/page_two` instead of the generic homepage `/`.
|
|
38
|
+
|
|
39
|
+
### Code Implementation Example:
|
|
40
|
+
```typescript
|
|
41
|
+
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
42
|
+
|
|
43
|
+
export default function LoginPage() {
|
|
44
|
+
const navigate = useNavigate();
|
|
45
|
+
const [searchParams] = useSearchParams();
|
|
46
|
+
|
|
47
|
+
const handleLoginSuccess = () => {
|
|
48
|
+
// 1. Read the redirectTo value (e.g., "/page_two") or default to "/"
|
|
49
|
+
const destination = searchParams.get("redirectTo") || "/";
|
|
50
|
+
|
|
51
|
+
// 2. Redirect the user back to their bookmarked page instead of the root page!
|
|
52
|
+
navigate(destination, { replace: true });
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export function PublicRoute() {
|
|
4
|
+
return <Outlet />;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
|
|
9
|
+
# NOTE : use this when you want to redirect user to main page if he is already logged in
|
|
10
|
+
|
|
11
|
+
import { Navigate, Outlet } from "react-router-dom";
|
|
12
|
+
|
|
13
|
+
export default function PublicRoute() {
|
|
14
|
+
const token = getLocalAuthToken();
|
|
15
|
+
if (token) {
|
|
16
|
+
return <Navigate to={PATH.desired_path_name} replace />;
|
|
17
|
+
}
|
|
18
|
+
return <Outlet />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
*/
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createBrowserRouter, RouteObject } from "react-router-dom";
|
|
2
|
+
import { PATH } from "./path";
|
|
3
|
+
import { PublicRoute } from "./guards/public.route";
|
|
4
|
+
import { PrivateRoute } from "./guards/private.route";
|
|
5
|
+
import { AuthLayout } from "./layouts/auth.layout";
|
|
6
|
+
import { ErrorLayout } from "./layouts/error.layout";
|
|
7
|
+
import { MainLayout } from "./layouts/main.layout";
|
|
8
|
+
|
|
9
|
+
const PublicRoutes: RouteObject[] = [
|
|
10
|
+
{
|
|
11
|
+
Component: PublicRoute,
|
|
12
|
+
children: [
|
|
13
|
+
{
|
|
14
|
+
Component: AuthLayout,
|
|
15
|
+
ErrorBoundary: ErrorLayout,
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
path: PATH.page_auth(),
|
|
19
|
+
lazy: async () => {
|
|
20
|
+
const module = await import("../routes/page-auth");
|
|
21
|
+
return { Component: module.default };
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const PrivateRoutes: RouteObject[] = [
|
|
31
|
+
{
|
|
32
|
+
Component: PrivateRoute,
|
|
33
|
+
children: [
|
|
34
|
+
{
|
|
35
|
+
Component: MainLayout,
|
|
36
|
+
ErrorBoundary: ErrorLayout,
|
|
37
|
+
children: [
|
|
38
|
+
{
|
|
39
|
+
path: PATH.page_root(),
|
|
40
|
+
ErrorBoundary: ErrorLayout,
|
|
41
|
+
lazy: async () => {
|
|
42
|
+
const module = await import("../routes/page-root");
|
|
43
|
+
return { Component: module.default };
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
path: PATH.page_one(),
|
|
48
|
+
ErrorBoundary: ErrorLayout,
|
|
49
|
+
lazy: async () => {
|
|
50
|
+
const module = await import("../routes/page-one");
|
|
51
|
+
return { Component: module.default };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: PATH.page_two(),
|
|
56
|
+
ErrorBoundary: ErrorLayout,
|
|
57
|
+
lazy: async () => {
|
|
58
|
+
const module = await import("../routes/page-two");
|
|
59
|
+
return { Component: module.default };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const NotFoundRoutes: RouteObject[] = [
|
|
69
|
+
{
|
|
70
|
+
path: PATH.not_found(), // Wildcard catch-all must be at the end of the root array
|
|
71
|
+
lazy: async () => {
|
|
72
|
+
const module = await import("../routes/not-found");
|
|
73
|
+
return { Component: module.default };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export const router = createBrowserRouter([
|
|
79
|
+
...PublicRoutes,
|
|
80
|
+
...PrivateRoutes,
|
|
81
|
+
...NotFoundRoutes,
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
|
|
86
|
+
currently i am using new way of using react-router version like
|
|
87
|
+
Component: PrivateLayout,
|
|
88
|
+
lazy: async () => ...
|
|
89
|
+
|
|
90
|
+
the old way is
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
path: "/",
|
|
94
|
+
element: (
|
|
95
|
+
<PublicRoute>
|
|
96
|
+
<AuthLayout />
|
|
97
|
+
</PublicRoute>
|
|
98
|
+
),
|
|
99
|
+
children: [
|
|
100
|
+
{
|
|
101
|
+
path: PATHS.LOGIN,
|
|
102
|
+
element: <Login />,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
| Feature | `errorElement` | `ErrorBoundary` |
|
|
110
|
+
| -------------------- | --------------------------------- | -------------------------- |
|
|
111
|
+
| What you pass | React Element | Component Function |
|
|
112
|
+
| Syntax | `errorElement: <ErrorPage />` | `ErrorBoundary: ErrorPage` |
|
|
113
|
+
| Requires JSX | Yes | No |
|
|
114
|
+
| Works in `.ts` file | No (unless `React.createElement`) | Yes |
|
|
115
|
+
| React Router version | v6.4+ | Newer preferred API |
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
# ANCHOR : 2 ways to show the error boundry page data
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# NOTE : WAY-1 inside layout i.e inplace of outlet
|
|
124
|
+
{
|
|
125
|
+
path: PATH.page_two(),
|
|
126
|
+
ErrorBoundary: ErrorLayout,
|
|
127
|
+
lazy: async () => {
|
|
128
|
+
const module = await import("../routes/page-two");
|
|
129
|
+
return { Component: module.default };
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
# NOTE : WAY-2 replace entire layout with error boundary
|
|
136
|
+
{
|
|
137
|
+
Component: AuthLayout,
|
|
138
|
+
ErrorBoundary: ErrorLayout,
|
|
139
|
+
children: [
|
|
140
|
+
{
|
|
141
|
+
path: PATH.page_auth(),
|
|
142
|
+
lazy: async () => {
|
|
143
|
+
const module = await import("../routes/page-auth");
|
|
144
|
+
return { Component: module.default };
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
# NOTE : DYNAMIC PAGE TITLES (SEO BEST PRACTICE)
|
|
153
|
+
To set browser tab titles dynamically on page transitions, create a custom hook and call it inside your page route components:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { useEffect } from "react";
|
|
157
|
+
|
|
158
|
+
export function useDocumentTitle(title: string) {
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const original = document.title;
|
|
161
|
+
document.title = `${title} | My App`;
|
|
162
|
+
return () => {
|
|
163
|
+
document.title = original;
|
|
164
|
+
};
|
|
165
|
+
}, [title]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// In routes/page-one.tsx:
|
|
169
|
+
// useDocumentTitle("Analytics Page");
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
# NOTE : ROUTE-LEVEL SUSPENSE VS. COMPONENT-LEVEL SKELETONS
|
|
175
|
+
|
|
176
|
+
1. **Why we place `<Suspense>` at the very top (wrapping `<RouterProvider>`):**
|
|
177
|
+
- **Code Chunk Loading:** Because we lazy-load routes (`lazy: async () => ...`), React Router has to download the page's `.js` chunk file from the server when navigating.
|
|
178
|
+
- **Preventing UI Freezes:** If you click a link and do not have a top-level Suspense boundary, the app will freeze or flicker on the old page until the file download finishes.
|
|
179
|
+
- **Best Practice:** Wrap the router in `<Suspense fallback={<TopBarProgressBar />} />` so the user gets instant progress feedback when clicking links.
|
|
180
|
+
|
|
181
|
+
2. **Why we use Component-level skeletons:**
|
|
182
|
+
- **Data Loading:** Once the page code is downloaded and mounted, your API data fetching starts.
|
|
183
|
+
- **Interactive UI:** This is where you render custom skeleton loaders inside your page components while waiting for TanStack Query data, offering a premium and localized loading layout.
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Outlet, useRouteError } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
export function ErrorLayout() {
|
|
4
|
+
const error = useRouteError();
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<h1>Something went wrong!</h1>
|
|
8
|
+
<p>
|
|
9
|
+
{error instanceof Error
|
|
10
|
+
? error.message
|
|
11
|
+
: "An unexpected error occurred"}
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export default function PageOne() {
|
|
2
|
+
return <div>Page One</div>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
# NOTE: FRAMEWORK-LEVEL META & PRERENDERING CONFIGURATION (SEO)
|
|
7
|
+
|
|
8
|
+
If you are using React Router v7 and want static HTML files compiled for SEO search crawlers, follow these steps:
|
|
9
|
+
|
|
10
|
+
-------------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
1. ROUTE META EXPORT (Add this inside this page file):
|
|
13
|
+
```typescript
|
|
14
|
+
import type { MetaFunction } from "react-router-dom";
|
|
15
|
+
|
|
16
|
+
export const meta: MetaFunction = () => {
|
|
17
|
+
return [
|
|
18
|
+
{ title: "Page One Analytics | My App" },
|
|
19
|
+
{ name: "description", content: "Analyze product sales and user logs." },
|
|
20
|
+
];
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
-------------------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
2. PRERENDER SETTINGS (Add this in your react-router.config.ts / vite.config.ts):
|
|
27
|
+
```typescript
|
|
28
|
+
export default {
|
|
29
|
+
async prerender() {
|
|
30
|
+
return ["/", "/page_one", "/page_two"]; // Routes to generate as static HTML pages
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
-------------------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
3. HOW THE BUILD CHANGES (Build output comparison):
|
|
38
|
+
|
|
39
|
+
OLD BUILD (Standard Single Page Application):
|
|
40
|
+
- Outputs only a single empty "index.html" page.
|
|
41
|
+
- Crawlers see no initial page HTML or titles when visiting "/page_one" until JS loads.
|
|
42
|
+
|
|
43
|
+
NEW BUILD (Prerendered SPA):
|
|
44
|
+
- Outputs directory folders containing index.html files:
|
|
45
|
+
dist/
|
|
46
|
+
├── index.html <- for "/"
|
|
47
|
+
├── page_one/
|
|
48
|
+
│ └── index.html <- for "/page_one" (contains pre-rendered metadata)
|
|
49
|
+
└── page_two/
|
|
50
|
+
└── index.html <- for "/page_two" (contains pre-rendered metadata)
|
|
51
|
+
|
|
52
|
+
-------------------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
4. SERVER SETTINGS (VPS Deployment):
|
|
55
|
+
|
|
56
|
+
For a standard VPS server (like Nginx), configure it to serve static files from your build directory.
|
|
57
|
+
Nginx will look for the folder-based index files (e.g., /page_one/index.html) first.
|
|
58
|
+
|
|
59
|
+
NGINX VPS CONFIGURATION EXAMPLE (/etc/nginx/sites-available/default):
|
|
60
|
+
```nginx
|
|
61
|
+
server {
|
|
62
|
+
listen 80;
|
|
63
|
+
server_name your-app.com;
|
|
64
|
+
|
|
65
|
+
# Point to your build distribution directory on the VPS
|
|
66
|
+
root /var/www/your-app/dist;
|
|
67
|
+
|
|
68
|
+
index index.html;
|
|
69
|
+
|
|
70
|
+
location / {
|
|
71
|
+
# 1. Checks if exact file exists ($uri)
|
|
72
|
+
# 2. Checks if a directory index.html exists ($uri/) -> Serves pre-rendered SEO pages!
|
|
73
|
+
# 3. Falls back to root index.html if route is handled client-side
|
|
74
|
+
try_files $uri $uri/ /index.html;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Optional: Proxy API requests directly to your Laravel Backend
|
|
78
|
+
location /api/ {
|
|
79
|
+
proxy_pass http://127.0.0.1:8000; # Address where your Laravel backend runs
|
|
80
|
+
proxy_set_header Host $host;
|
|
81
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
*/
|