olovaplugin 1.0.13 → 1.0.15
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 +26 -17
- package/dist/auto-generate.d.ts +3 -0
- package/dist/auto-generate.d.ts.map +1 -0
- package/dist/auto-generate.js +114 -0
- package/dist/auto-generate.js.map +1 -0
- package/dist/clean-url.d.ts +3 -0
- package/dist/clean-url.d.ts.map +1 -0
- package/dist/clean-url.js +40 -0
- package/dist/clean-url.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +99 -0
- package/dist/config.js.map +1 -0
- package/dist/error-overlay.d.ts +3 -0
- package/dist/error-overlay.d.ts.map +1 -0
- package/dist/error-overlay.js +59 -0
- package/dist/error-overlay.js.map +1 -0
- package/dist/framework.d.ts +3 -0
- package/dist/framework.d.ts.map +1 -0
- package/dist/framework.js +41 -0
- package/dist/framework.js.map +1 -0
- package/dist/hydration.d.ts +19 -0
- package/dist/hydration.d.ts.map +1 -0
- package/dist/hydration.js +221 -0
- package/dist/hydration.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +165 -0
- package/dist/router.js.map +1 -0
- package/dist/runtime/router.d.ts +81 -0
- package/dist/runtime/router.d.ts.map +1 -0
- package/dist/runtime/router.js +284 -0
- package/dist/runtime/router.js.map +1 -0
- package/dist/ssg.d.ts +3 -0
- package/dist/ssg.d.ts.map +1 -0
- package/dist/ssg.js +376 -0
- package/dist/ssg.js.map +1 -0
- package/dist/terminal.d.ts +68 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +143 -0
- package/dist/terminal.js.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +68 -0
- package/dist/utils.js.map +1 -0
- package/dist/virtual-html.d.ts +3 -0
- package/dist/virtual-html.d.ts.map +1 -0
- package/dist/virtual-html.js +178 -0
- package/dist/virtual-html.js.map +1 -0
- package/package.json +33 -30
- package/runtime/client.tsx +119 -0
- package/runtime/index.ts +18 -0
- package/runtime/router.tsx +381 -0
- package/runtime/server.tsx +110 -0
- package/dist/olova-plugins.cjs +0 -1683
- package/dist/olova-plugins.d.cts +0 -123
- package/dist/olova-plugins.d.ts +0 -123
- package/dist/olova-plugins.js +0 -1622
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import React, { useState, useEffect, createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// @ts-ignore - Virtual module generated by olovaplugin
|
|
4
|
+
import { routes } from 'olova/routes';
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// ROUTER CONTEXT
|
|
8
|
+
// =============================================================================
|
|
9
|
+
const RouterContext = createContext<{ params: Record<string, string>, path: string }>({ params: {}, path: '/' });
|
|
10
|
+
|
|
11
|
+
export function useParams() {
|
|
12
|
+
return useContext(RouterContext).params;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function usePath() {
|
|
16
|
+
return useContext(RouterContext).path;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the current pathname (without query string or hash)
|
|
21
|
+
* Similar to Next.js usePathname hook
|
|
22
|
+
*/
|
|
23
|
+
export function usePathname(): string {
|
|
24
|
+
const [pathname, setPathname] = useState(() => {
|
|
25
|
+
if (typeof window === 'undefined') return '/';
|
|
26
|
+
return window.location.pathname;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (typeof window === 'undefined') return;
|
|
31
|
+
|
|
32
|
+
const handleNavigation = () => {
|
|
33
|
+
setPathname(window.location.pathname);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
window.addEventListener('popstate', handleNavigation);
|
|
37
|
+
window.addEventListener('pushstate', handleNavigation);
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener('popstate', handleNavigation);
|
|
41
|
+
window.removeEventListener('pushstate', handleNavigation);
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return pathname;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A read-only interface for URLSearchParams
|
|
50
|
+
* Similar to Next.js useSearchParams hook
|
|
51
|
+
*/
|
|
52
|
+
interface ReadonlyURLSearchParams {
|
|
53
|
+
get(name: string): string | null;
|
|
54
|
+
getAll(name: string): string[];
|
|
55
|
+
has(name: string): boolean;
|
|
56
|
+
keys(): IterableIterator<string>;
|
|
57
|
+
values(): IterableIterator<string>;
|
|
58
|
+
entries(): IterableIterator<[string, string]>;
|
|
59
|
+
forEach(callback: (value: string, key: string, parent: URLSearchParams) => void): void;
|
|
60
|
+
toString(): string;
|
|
61
|
+
size: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the current URL search parameters
|
|
66
|
+
* Similar to Next.js useSearchParams hook
|
|
67
|
+
*/
|
|
68
|
+
export function useSearchParams(): ReadonlyURLSearchParams {
|
|
69
|
+
const [searchParams, setSearchParams] = useState<URLSearchParams>(() => {
|
|
70
|
+
if (typeof window === 'undefined') return new URLSearchParams();
|
|
71
|
+
return new URLSearchParams(window.location.search);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (typeof window === 'undefined') return;
|
|
76
|
+
|
|
77
|
+
const handleNavigation = () => {
|
|
78
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
window.addEventListener('popstate', handleNavigation);
|
|
82
|
+
window.addEventListener('pushstate', handleNavigation);
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
window.removeEventListener('popstate', handleNavigation);
|
|
86
|
+
window.removeEventListener('pushstate', handleNavigation);
|
|
87
|
+
};
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
get: (name: string) => searchParams.get(name),
|
|
92
|
+
getAll: (name: string) => searchParams.getAll(name),
|
|
93
|
+
has: (name: string) => searchParams.has(name),
|
|
94
|
+
keys: () => searchParams.keys(),
|
|
95
|
+
values: () => searchParams.values(),
|
|
96
|
+
entries: () => searchParams.entries(),
|
|
97
|
+
forEach: (callback) => searchParams.forEach(callback),
|
|
98
|
+
toString: () => searchParams.toString(),
|
|
99
|
+
get size() { return Array.from(searchParams.keys()).length; }
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// ROUTE MATCHING
|
|
105
|
+
// =============================================================================
|
|
106
|
+
const normalizePath = (path: string) => {
|
|
107
|
+
let p = path.split('?')[0].split('#')[0];
|
|
108
|
+
if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
|
|
109
|
+
return p || '/';
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export function matchRoute(path: string) {
|
|
113
|
+
const normalizedPath = normalizePath(path);
|
|
114
|
+
const routeKeys = Object.keys(routes);
|
|
115
|
+
|
|
116
|
+
for (const route of routeKeys) {
|
|
117
|
+
if (route === normalizedPath) {
|
|
118
|
+
return { loader: routes[route], params: {}, pattern: route };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const regexPath = route
|
|
122
|
+
.replace(/:[^\/]+/g, '([^/]+)')
|
|
123
|
+
.replace(/\$[^\/]+/g, '([^/]+)');
|
|
124
|
+
|
|
125
|
+
const regex = new RegExp(`^${regexPath}$`);
|
|
126
|
+
const match = normalizedPath.match(regex);
|
|
127
|
+
|
|
128
|
+
if (match) {
|
|
129
|
+
const params: Record<string, string> = {};
|
|
130
|
+
const paramNames = (route.match(/[:$][^\/]+/g) || []).map(s => s.slice(1));
|
|
131
|
+
paramNames.forEach((name, i) => {
|
|
132
|
+
params[name] = match[i + 1];
|
|
133
|
+
});
|
|
134
|
+
return { loader: routes[route], params, pattern: route };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Metadata type - like Next.js
|
|
141
|
+
export interface Metadata {
|
|
142
|
+
title?: string;
|
|
143
|
+
description?: string;
|
|
144
|
+
keywords?: string | string[];
|
|
145
|
+
openGraph?: {
|
|
146
|
+
title?: string;
|
|
147
|
+
description?: string;
|
|
148
|
+
url?: string;
|
|
149
|
+
siteName?: string;
|
|
150
|
+
images?: { url: string; width?: number; height?: number; alt?: string }[];
|
|
151
|
+
type?: string;
|
|
152
|
+
};
|
|
153
|
+
twitter?: {
|
|
154
|
+
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
155
|
+
site?: string;
|
|
156
|
+
creator?: string;
|
|
157
|
+
title?: string;
|
|
158
|
+
description?: string;
|
|
159
|
+
images?: string[];
|
|
160
|
+
};
|
|
161
|
+
robots?: string;
|
|
162
|
+
canonical?: string;
|
|
163
|
+
jsonLd?: object | object[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Simple markdown to HTML converter
|
|
167
|
+
function parseMarkdown(md: string): string {
|
|
168
|
+
return md
|
|
169
|
+
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
|
170
|
+
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
171
|
+
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
172
|
+
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
173
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
174
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
175
|
+
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
|
176
|
+
.replace(/`(.*?)`/g, '<code>$1</code>')
|
|
177
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
178
|
+
.replace(/\n\n/g, '</p><p>')
|
|
179
|
+
.replace(/\n/g, '<br>')
|
|
180
|
+
.replace(/^(.*)$/, '<p>$1</p>');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function HtmlContent({ html }: { html: string }) {
|
|
184
|
+
return React.createElement('div', {
|
|
185
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
186
|
+
className: 'olova-html-content'
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function MarkdownContent({ markdown }: { markdown: string }) {
|
|
191
|
+
const html = parseMarkdown(markdown);
|
|
192
|
+
return React.createElement('article', {
|
|
193
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
194
|
+
className: 'olova-markdown-content'
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function loadRoute(path: string) {
|
|
199
|
+
const match = matchRoute(path);
|
|
200
|
+
if (match) {
|
|
201
|
+
console.log(`[Router] Loading route: ${path} (pattern: ${match.pattern})`);
|
|
202
|
+
const module = await match.loader();
|
|
203
|
+
|
|
204
|
+
if (module.__isHtml) {
|
|
205
|
+
return {
|
|
206
|
+
module: {
|
|
207
|
+
default: () => HtmlContent({ html: module.__rawHtml as string }),
|
|
208
|
+
},
|
|
209
|
+
params: match.params,
|
|
210
|
+
metadata: undefined
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (module.__isMd) {
|
|
215
|
+
return {
|
|
216
|
+
module: {
|
|
217
|
+
default: () => MarkdownContent({ markdown: module.default as unknown as string }),
|
|
218
|
+
},
|
|
219
|
+
params: match.params,
|
|
220
|
+
metadata: undefined
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
module,
|
|
226
|
+
params: match.params,
|
|
227
|
+
metadata: module.metadata as Metadata | undefined
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
console.warn(`[Router] No match for: ${path}`);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// =============================================================================
|
|
235
|
+
// ROUTER COMPONENT
|
|
236
|
+
// =============================================================================
|
|
237
|
+
interface RouterProps {
|
|
238
|
+
url?: string;
|
|
239
|
+
initialComponent?: React.ComponentType;
|
|
240
|
+
initialParams?: Record<string, string>;
|
|
241
|
+
onRouteChange?: (metadata: Metadata | undefined) => void;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function Router({ url, initialComponent, initialParams, onRouteChange }: RouterProps) {
|
|
245
|
+
const [path, setPath] = useState(() => normalizePath(url || (typeof window !== 'undefined' ? window.location.pathname : '/')));
|
|
246
|
+
const [Component, setComponent] = useState<React.ComponentType | null>(() => initialComponent || null);
|
|
247
|
+
const [params, setParams] = useState<Record<string, string>>(() => initialParams || {});
|
|
248
|
+
const hasHydrated = React.useRef(false);
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (typeof window === 'undefined') return;
|
|
252
|
+
|
|
253
|
+
const handleNavigation = () => {
|
|
254
|
+
const newPath = normalizePath(window.location.pathname);
|
|
255
|
+
console.log(`[Router] Navigation event: ${newPath}`);
|
|
256
|
+
setPath(newPath);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
window.addEventListener('popstate', handleNavigation);
|
|
260
|
+
window.addEventListener('pushstate', handleNavigation);
|
|
261
|
+
|
|
262
|
+
return () => {
|
|
263
|
+
window.removeEventListener('popstate', handleNavigation);
|
|
264
|
+
window.removeEventListener('pushstate', handleNavigation);
|
|
265
|
+
};
|
|
266
|
+
}, []);
|
|
267
|
+
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (!hasHydrated.current && initialComponent && path === normalizePath(url || '')) {
|
|
270
|
+
console.log(`[Router] Hydration skipped for: ${path}`);
|
|
271
|
+
hasHydrated.current = true;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let isCancelled = false;
|
|
276
|
+
const load = async () => {
|
|
277
|
+
const result = await loadRoute(path);
|
|
278
|
+
if (!isCancelled) {
|
|
279
|
+
if (result) {
|
|
280
|
+
setComponent(() => result.module.default);
|
|
281
|
+
setParams(result.params);
|
|
282
|
+
if (onRouteChange) {
|
|
283
|
+
onRouteChange(result.metadata);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
const fallbackResult = await loadRoute('/404');
|
|
287
|
+
if (fallbackResult) {
|
|
288
|
+
console.log('[Router] Serving custom 404 page');
|
|
289
|
+
setComponent(() => fallbackResult.module.default);
|
|
290
|
+
setParams(fallbackResult.params);
|
|
291
|
+
if (onRouteChange) {
|
|
292
|
+
onRouteChange(fallbackResult.metadata);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
setComponent(() => () => <div>404 Not Found</div>);
|
|
296
|
+
setParams({});
|
|
297
|
+
if (onRouteChange) {
|
|
298
|
+
onRouteChange(undefined);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
load();
|
|
306
|
+
return () => { isCancelled = true; };
|
|
307
|
+
}, [path, onRouteChange]);
|
|
308
|
+
|
|
309
|
+
if (!Component) return <div>Loading...</div>;
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<RouterContext.Provider value={{ params, path }}>
|
|
313
|
+
<Component {...params} />
|
|
314
|
+
</RouterContext.Provider>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// =============================================================================
|
|
319
|
+
// LINK COMPONENT
|
|
320
|
+
// =============================================================================
|
|
321
|
+
export function Link({ href, children, ...props }: { href: string, children: ReactNode } & React.AnchorHTMLAttributes<HTMLAnchorElement>): React.ReactElement {
|
|
322
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
323
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
324
|
+
if (href.startsWith('http') || href.startsWith('//')) return;
|
|
325
|
+
|
|
326
|
+
e.preventDefault();
|
|
327
|
+
|
|
328
|
+
const currentUrl = window.location.pathname + window.location.search;
|
|
329
|
+
const targetUrl = href.startsWith('/') ? href : '/' + href;
|
|
330
|
+
|
|
331
|
+
if (currentUrl === targetUrl) return;
|
|
332
|
+
|
|
333
|
+
window.history.pushState({}, '', href);
|
|
334
|
+
window.dispatchEvent(new Event('pushstate'));
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<a href={href} onClick={handleClick} {...props}>
|
|
339
|
+
{children}
|
|
340
|
+
</a>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// =============================================================================
|
|
345
|
+
// HYDRATION UTILITIES
|
|
346
|
+
// =============================================================================
|
|
347
|
+
export function parseFlightData(): Record<string, unknown> | null {
|
|
348
|
+
if (typeof globalThis === 'undefined' || typeof (globalThis as Record<string, unknown>).document === 'undefined') {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const flightArray = ((globalThis as Record<string, unknown>).__olova_f) as unknown[] | undefined;
|
|
353
|
+
if (!flightArray) return null;
|
|
354
|
+
|
|
355
|
+
type FlightDataType = 'M' | 'T' | 'R' | 'P' | 'A' | 'S' | 'D' | 'H' | 'E';
|
|
356
|
+
|
|
357
|
+
const result: Record<string, unknown> = {};
|
|
358
|
+
const typeMap: Record<FlightDataType, string> = {
|
|
359
|
+
'M': '$meta',
|
|
360
|
+
'T': '$tree',
|
|
361
|
+
'R': '$route',
|
|
362
|
+
'P': '$params',
|
|
363
|
+
'A': '$assets',
|
|
364
|
+
'S': '$state',
|
|
365
|
+
'D': '$schema',
|
|
366
|
+
'H': '$hints',
|
|
367
|
+
'E': '$end'
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
for (const chunk of flightArray) {
|
|
371
|
+
if (Array.isArray(chunk) && chunk.length >= 3) {
|
|
372
|
+
const [_index, type, data] = chunk;
|
|
373
|
+
const key = typeMap[type as FlightDataType];
|
|
374
|
+
if (key && key !== '$end') {
|
|
375
|
+
result[key] = data;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OLOVA SERVER ENTRY - Handles SSG/SSR rendering
|
|
3
|
+
// Real TSX file served via Vite alias
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import { renderToString } from 'react-dom/server';
|
|
7
|
+
// @ts-ignore - User's root layout
|
|
8
|
+
import Layout, { metadata as defaultMetadata } from '/src/root.tsx';
|
|
9
|
+
import { Router, loadRoute, type Metadata } from './router';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// SEO HEAD GENERATION
|
|
13
|
+
// =============================================================================
|
|
14
|
+
function generateSeoHead(metadata: Metadata | undefined): string {
|
|
15
|
+
const meta = { ...defaultMetadata, ...metadata } as Metadata;
|
|
16
|
+
let head = '';
|
|
17
|
+
|
|
18
|
+
if (meta.title) {
|
|
19
|
+
head += `<title>${meta.title}</title>\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (meta.description) {
|
|
23
|
+
head += `<meta name="description" content="${meta.description}" />\n`;
|
|
24
|
+
}
|
|
25
|
+
if (meta.keywords) {
|
|
26
|
+
const content = Array.isArray(meta.keywords) ? meta.keywords.join(', ') : meta.keywords;
|
|
27
|
+
head += `<meta name="keywords" content="${content}" />\n`;
|
|
28
|
+
}
|
|
29
|
+
if (meta.robots) {
|
|
30
|
+
head += `<meta name="robots" content="${meta.robots}" />\n`;
|
|
31
|
+
}
|
|
32
|
+
if (meta.canonical) {
|
|
33
|
+
head += `<link rel="canonical" href="${meta.canonical}" />\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (meta.openGraph) {
|
|
37
|
+
const og = meta.openGraph;
|
|
38
|
+
head += `<meta property="og:title" content="${og.title || meta.title}" />\n`;
|
|
39
|
+
if (og.description) head += `<meta property="og:description" content="${og.description}" />\n`;
|
|
40
|
+
if (og.url) head += `<meta property="og:url" content="${og.url}" />\n`;
|
|
41
|
+
if (og.siteName) head += `<meta property="og:site_name" content="${og.siteName}" />\n`;
|
|
42
|
+
if (og.type) head += `<meta property="og:type" content="${og.type}" />\n`;
|
|
43
|
+
if (og.images) {
|
|
44
|
+
og.images.forEach(img => {
|
|
45
|
+
head += `<meta property="og:image" content="${img.url}" />\n`;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (meta.twitter) {
|
|
51
|
+
const tw = meta.twitter;
|
|
52
|
+
head += `<meta name="twitter:card" content="${tw.card || 'summary'}" />\n`;
|
|
53
|
+
if (tw.site) head += `<meta name="twitter:site" content="${tw.site}" />\n`;
|
|
54
|
+
if (tw.creator) head += `<meta name="twitter:creator" content="${tw.creator}" />\n`;
|
|
55
|
+
head += `<meta name="twitter:title" content="${tw.title || meta.title}" />\n`;
|
|
56
|
+
if (tw.description) head += `<meta name="twitter:description" content="${tw.description}" />\n`;
|
|
57
|
+
if (tw.images) {
|
|
58
|
+
tw.images.forEach(img => {
|
|
59
|
+
head += `<meta name="twitter:image" content="${img}" />\n`;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return head;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// SERVER RENDER FUNCTIONS
|
|
69
|
+
// =============================================================================
|
|
70
|
+
export async function render(url: string) {
|
|
71
|
+
const result = await loadRoute(url);
|
|
72
|
+
const Component = result ? result.module.default : () => <div>404 Not Found</div>;
|
|
73
|
+
const params = result ? result.params : {};
|
|
74
|
+
const metadata = result ? result.metadata : undefined;
|
|
75
|
+
|
|
76
|
+
const seoHead = generateSeoHead(metadata);
|
|
77
|
+
|
|
78
|
+
let html = renderToString(
|
|
79
|
+
<Layout>
|
|
80
|
+
<Router url={url} initialComponent={Component} initialParams={params} />
|
|
81
|
+
</Layout>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (html.includes('</head>')) {
|
|
85
|
+
html = html.replace('</head>', seoHead + '</head>');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { html, hydrationData: { params, metadata } };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderShell(): string {
|
|
92
|
+
const seoHead = generateSeoHead(undefined);
|
|
93
|
+
let html = renderToString(<Layout>{null}</Layout>);
|
|
94
|
+
if (html.includes('</head>')) {
|
|
95
|
+
html = html.replace('</head>', seoHead + '</head>');
|
|
96
|
+
}
|
|
97
|
+
return html;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderShellWithMetadata(metadata: Metadata | undefined): string {
|
|
101
|
+
const seoHead = generateSeoHead(metadata);
|
|
102
|
+
let html = renderToString(<Layout>{null}</Layout>);
|
|
103
|
+
if (html.includes('</head>')) {
|
|
104
|
+
html = html.replace('</head>', seoHead + '</head>');
|
|
105
|
+
}
|
|
106
|
+
return html;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Re-export loadRoute for SSG
|
|
110
|
+
export { loadRoute };
|