lovable-ssr 0.1.18 → 0.1.19
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 +4 -1
- package/dist/components/BrowserRouteDataProvider.d.ts +1 -1
- package/dist/components/BrowserRouteDataProvider.d.ts.map +1 -1
- package/dist/components/BrowserRouteDataProvider.js +3 -2
- package/dist/components/SEO.d.ts +15 -0
- package/dist/components/SEO.d.ts.map +1 -0
- package/dist/components/SEO.js +9 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/ssr/render.d.ts +6 -0
- package/dist/ssr/render.d.ts.map +1 -1
- package/dist/ssr/render.js +12 -2
- package/dist/ssr/server.d.ts +12 -0
- package/dist/ssr/server.d.ts.map +1 -1
- package/dist/ssr/server.js +84 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# lovable-ssr
|
|
2
2
|
|
|
3
|
-
SSR and route data engine for [Lovable](https://lovable.dev) projects: route registry, `
|
|
3
|
+
SSR and route data engine for [Lovable](https://lovable.dev) projects: route registry, `getData`, Express + Vite server, and built-in SEO support.
|
|
4
4
|
|
|
5
5
|
**Documentation:** [Documentação completa](https://calm-meadow-5cf6.github-8c8.workers.dev/)
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@ SSR and route data engine for [Lovable](https://lovable.dev) projects: route reg
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm i lovable-ssr
|
|
11
|
+
# or
|
|
12
|
+
yarn add lovable-ssr
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
Peer dependencies: `react`, `react-dom`, `react-router-dom` (^18 / ^6).
|
|
@@ -17,6 +19,7 @@ Peer dependencies: `react`, `react-dom`, `react-router-dom` (^18 / ^6).
|
|
|
17
19
|
1. Register routes with `registerRoutes(routes)`.
|
|
18
20
|
2. Wrap your app with `BrowserRouteDataProvider` and render `AppRoutes` (inside `BrowserRouter`).
|
|
19
21
|
3. (Optional) SSR: entry module + `createServer` from `lovable-ssr/server`.
|
|
22
|
+
4. (Optional) SEO: add `<SEO />` on pages to set meta tags and JSON-LD; helmet content is injected during SSR.
|
|
20
23
|
|
|
21
24
|
Details, examples, and API: see the [documentation](https://calm-meadow-5cf6.github-8c8.workers.dev/).
|
|
22
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* Wraps children with RouteDataProvider using initial data from the browser:
|
|
3
|
+
* Wraps children with HelmetProvider and RouteDataProvider using initial data from the browser:
|
|
4
4
|
* - window.__PRELOADED_DATA__ (from SSR)
|
|
5
5
|
* - window.location.pathname + RouterService for matchedRoute and routeParams
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrowserRouteDataProvider.d.ts","sourceRoot":"","sources":["../../src/components/BrowserRouteDataProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"BrowserRouteDataProvider.d.ts","sourceRoot":"","sources":["../../src/components/BrowserRouteDataProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CA4B7E"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
2
3
|
import RouterService from '../router/RouterService.js';
|
|
3
4
|
import { RouteDataProvider } from '../router/RouteDataContext.js';
|
|
4
5
|
/**
|
|
5
|
-
* Wraps children with RouteDataProvider using initial data from the browser:
|
|
6
|
+
* Wraps children with HelmetProvider and RouteDataProvider using initial data from the browser:
|
|
6
7
|
* - window.__PRELOADED_DATA__ (from SSR)
|
|
7
8
|
* - window.location.pathname + RouterService for matchedRoute and routeParams
|
|
8
9
|
*
|
|
@@ -21,5 +22,5 @@ export function BrowserRouteDataProvider({ children }) {
|
|
|
21
22
|
routeParams: routeParamsResult.routeParams,
|
|
22
23
|
searchParams: Object.keys(searchParams).length > 0 ? searchParams : routeParamsResult.searchParams,
|
|
23
24
|
};
|
|
24
|
-
return (_jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: initialParams, children: children }));
|
|
25
|
+
return (_jsx(HelmetProvider, { children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: initialParams, children: children }) }));
|
|
25
26
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SEOProps {
|
|
2
|
+
title: string;
|
|
3
|
+
description: string;
|
|
4
|
+
image?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
noindex?: boolean;
|
|
8
|
+
structuredData?: object;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Centralized SEO component for meta tags and JSON-LD.
|
|
12
|
+
* Use within HelmetProvider (provided by framework at app level).
|
|
13
|
+
*/
|
|
14
|
+
export declare function SEO({ title, description, image, url, type, noindex, structuredData, }: SEOProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=SEO.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SEO.d.ts","sourceRoot":"","sources":["../../src/components/SEO.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,EAClB,KAAK,EACL,WAAW,EACX,KAAK,EACL,GAAG,EACH,IAAgB,EAChB,OAAe,EACf,cAAc,GACf,EAAE,QAAQ,2CAuBV"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Helmet } from 'react-helmet-async';
|
|
3
|
+
/**
|
|
4
|
+
* Centralized SEO component for meta tags and JSON-LD.
|
|
5
|
+
* Use within HelmetProvider (provided by framework at app level).
|
|
6
|
+
*/
|
|
7
|
+
export function SEO({ title, description, image, url, type = 'website', noindex = false, structuredData, }) {
|
|
8
|
+
return (_jsxs(Helmet, { children: [_jsx("title", { children: title }), _jsx("meta", { name: "description", content: description }), noindex && _jsx("meta", { name: "robots", content: "noindex, nofollow" }), url && _jsx("link", { rel: "canonical", href: url }), url && _jsx("meta", { property: "og:url", content: url }), _jsx("meta", { property: "og:title", content: title }), _jsx("meta", { property: "og:description", content: description }), _jsx("meta", { property: "og:type", content: type }), image && _jsx("meta", { property: "og:image", content: image }), _jsx("meta", { name: "twitter:card", content: "summary_large_image" }), _jsx("meta", { name: "twitter:title", content: title }), _jsx("meta", { name: "twitter:description", content: description }), image && _jsx("meta", { name: "twitter:image", content: image }), structuredData && (_jsx("script", { type: "application/ld+json", children: JSON.stringify(structuredData) }))] }));
|
|
9
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import './globals.js';
|
|
2
|
-
export type { RouteConfig, ComponentWithGetData, RouteDataParams } from './types.js';
|
|
2
|
+
export type { RouteConfig, ComponentWithGetData, RouteDataParams, SitemapEntry, SitemapRouteConfig, SitemapChangefreq, } from './types.js';
|
|
3
3
|
export { registerRoutes, getRoutes } from './registry.js';
|
|
4
4
|
export { BrowserRouteDataProvider } from './components/BrowserRouteDataProvider.js';
|
|
5
|
+
export { SEO, type SEOProps } from './components/SEO.js';
|
|
5
6
|
export { default as RouterService } from './router/RouterService.js';
|
|
6
7
|
export { RouteDataProvider, useRouteData, buildRouteKey, type RouteDataState, type InitialRouteShape, } from './router/RouteDataContext.js';
|
|
7
8
|
export { AppRoutes } from './components/AppRoutes.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AACtB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AACtB,YAAY,EACV,WAAW,EACX,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AACpF,OAAO,EAAE,GAAG,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EACL,MAAM,EACN,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import './globals.js';
|
|
2
2
|
export { registerRoutes, getRoutes } from './registry.js';
|
|
3
3
|
export { BrowserRouteDataProvider } from './components/BrowserRouteDataProvider.js';
|
|
4
|
+
export { SEO } from './components/SEO.js';
|
|
4
5
|
export { default as RouterService } from './router/RouterService.js';
|
|
5
6
|
export { RouteDataProvider, useRouteData, buildRouteKey, } from './router/RouteDataContext.js';
|
|
6
7
|
export { AppRoutes } from './components/AppRoutes.js';
|
package/dist/ssr/render.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { RequestContext } from '../types.js';
|
|
|
3
3
|
export interface RenderResult {
|
|
4
4
|
html: string;
|
|
5
5
|
preloadedData: Record<string, unknown>;
|
|
6
|
+
helmet?: {
|
|
7
|
+
title: string;
|
|
8
|
+
meta: string;
|
|
9
|
+
link: string;
|
|
10
|
+
script: string;
|
|
11
|
+
};
|
|
6
12
|
}
|
|
7
13
|
export interface RenderOptions {
|
|
8
14
|
wrap?: (children: ReactNode) => ReactNode;
|
package/dist/ssr/render.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/ssr/render.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/ssr/render.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAOvC,OAAO,EAAE,cAAc,EAAmB,MAAM,aAAa,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAsB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAqDxF"}
|
package/dist/ssr/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
3
4
|
import { StaticRouter } from 'react-router-dom/server';
|
|
4
5
|
import { AppRoutes } from '../components/AppRoutes.js';
|
|
5
6
|
import { RouteDataProvider } from '../router/RouteDataContext.js';
|
|
@@ -32,8 +33,17 @@ export async function render(url, options) {
|
|
|
32
33
|
preloadedData = { ...preloadedData, is_success: false };
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
+
const helmetContext = {};
|
|
37
|
+
const inner = (_jsx(HelmetProvider, { context: helmetContext, children: _jsx(StaticRouter, { location: url, children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: params, children: _jsx(AppRoutes, {}) }) }) }));
|
|
36
38
|
const app = options?.wrap ? options.wrap(inner) : inner;
|
|
37
39
|
const html = renderToString(app);
|
|
38
|
-
|
|
40
|
+
const helmet = helmetContext.helmet
|
|
41
|
+
? {
|
|
42
|
+
title: helmetContext.helmet.title.toString(),
|
|
43
|
+
meta: helmetContext.helmet.meta.toString(),
|
|
44
|
+
link: helmetContext.helmet.link.toString(),
|
|
45
|
+
script: helmetContext.helmet.script.toString(),
|
|
46
|
+
}
|
|
47
|
+
: undefined;
|
|
48
|
+
return { html, preloadedData, helmet };
|
|
39
49
|
}
|
package/dist/ssr/server.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ import { type Express } from 'express';
|
|
|
2
2
|
export interface RenderResult {
|
|
3
3
|
html: string;
|
|
4
4
|
preloadedData: Record<string, unknown>;
|
|
5
|
+
helmet?: {
|
|
6
|
+
title: string;
|
|
7
|
+
meta: string;
|
|
8
|
+
link: string;
|
|
9
|
+
script: string;
|
|
10
|
+
};
|
|
5
11
|
}
|
|
6
12
|
export interface CreateServerConfig {
|
|
7
13
|
root: string;
|
|
@@ -10,6 +16,12 @@ export interface CreateServerConfig {
|
|
|
10
16
|
port?: number;
|
|
11
17
|
/** Optional link tag to inject in dev for CSS (e.g. '<link rel="stylesheet" href="/src/index.css">') */
|
|
12
18
|
cssLinkInDev?: string;
|
|
19
|
+
/** Register routes before the SSR catch-all (e.g. sitemap.xml, robots.txt) */
|
|
20
|
+
extraRoutes?: (app: Express) => void;
|
|
21
|
+
/** Enable sitemap.xml and robots.txt from route registry. Routes with sitemap.include are included. */
|
|
22
|
+
sitemap?: {
|
|
23
|
+
siteUrl: string;
|
|
24
|
+
};
|
|
13
25
|
}
|
|
14
26
|
export declare function createServer(config: CreateServerConfig): Promise<{
|
|
15
27
|
getApp: () => Express;
|
package/dist/ssr/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ssr/server.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,OAAO,EAAkD,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ssr/server.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,OAAO,EAAkD,MAAM,SAAS,CAAC;AAOhG,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wGAAwG;IACxG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,uGAAuG;IACvG,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/B;AAmVD,wBAAsB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC;IACtE,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACxD,CAAC,CAOD;AAED;qDACqD;AACrD,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,QAO7D"}
|
package/dist/ssr/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import { createServer as createViteServer } from 'vite';
|
|
6
|
+
import { getRoutes } from '../registry.js';
|
|
6
7
|
import RouterService from '../router/RouterService.js';
|
|
7
8
|
function defaultDistEntryPath(entryPath) {
|
|
8
9
|
return entryPath
|
|
@@ -30,6 +31,8 @@ class SsrServer {
|
|
|
30
31
|
entryPath: config.entryPath,
|
|
31
32
|
port: config.port ?? 5173,
|
|
32
33
|
cssLinkInDev: config.cssLinkInDev ?? '<link rel="stylesheet" href="/src/index.css"></head>',
|
|
34
|
+
extraRoutes: config.extraRoutes ?? (() => { }),
|
|
35
|
+
sitemap: config.sitemap ?? undefined,
|
|
33
36
|
};
|
|
34
37
|
this.isProd = process.env.NODE_ENV === 'production';
|
|
35
38
|
this.app = express();
|
|
@@ -65,8 +68,74 @@ class SsrServer {
|
|
|
65
68
|
this.app.use(express.static(path.join(this.config.root, 'dist'), { index: false }));
|
|
66
69
|
}
|
|
67
70
|
configureRequestHandler() {
|
|
71
|
+
if (this.config.sitemap?.siteUrl) {
|
|
72
|
+
this.registerSitemapRoutes(this.config.sitemap.siteUrl);
|
|
73
|
+
}
|
|
74
|
+
this.config.extraRoutes?.(this.app);
|
|
68
75
|
this.app.use('*', (req, res, next) => this.handleRequest(req, res, next));
|
|
69
76
|
}
|
|
77
|
+
registerSitemapRoutes(siteUrl) {
|
|
78
|
+
const baseUrl = siteUrl.replace(/\/$/, '');
|
|
79
|
+
this.app.get('/robots.txt', (_req, res) => {
|
|
80
|
+
res.type('text/plain');
|
|
81
|
+
res.send(`User-agent: *
|
|
82
|
+
Allow: /
|
|
83
|
+
|
|
84
|
+
Sitemap: ${baseUrl}/sitemap.xml`);
|
|
85
|
+
});
|
|
86
|
+
this.app.get('/sitemap.xml', async (_req, res) => {
|
|
87
|
+
const today = new Date().toISOString().split('T')[0];
|
|
88
|
+
const entries = [];
|
|
89
|
+
for (const route of getRoutes()) {
|
|
90
|
+
const cfg = route.sitemap;
|
|
91
|
+
if (!cfg?.include)
|
|
92
|
+
continue;
|
|
93
|
+
const changefreq = cfg.changefreq ?? 'weekly';
|
|
94
|
+
const priority = cfg.priority ?? 0.5;
|
|
95
|
+
if (cfg.getEntries) {
|
|
96
|
+
try {
|
|
97
|
+
const dynamicEntries = await cfg.getEntries({ siteUrl: baseUrl });
|
|
98
|
+
for (const e of dynamicEntries) {
|
|
99
|
+
entries.push({
|
|
100
|
+
loc: e.loc,
|
|
101
|
+
lastmod: e.lastmod ?? today,
|
|
102
|
+
changefreq: e.changefreq ?? changefreq,
|
|
103
|
+
priority: e.priority ?? priority,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error(`[lovable-ssr] sitemap getEntries failed for ${route.path}:`, err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (!route.path.includes(':')) {
|
|
112
|
+
entries.push({
|
|
113
|
+
loc: `${baseUrl}${route.path === '/' ? '' : route.path}`,
|
|
114
|
+
lastmod: today,
|
|
115
|
+
changefreq,
|
|
116
|
+
priority,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const xml = this.buildSitemapXml(entries);
|
|
121
|
+
res.type('application/xml').send(xml);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
buildSitemapXml(entries) {
|
|
125
|
+
const lines = entries.map((e) => ` <url><loc>${this.escapeXml(e.loc)}</loc><lastmod>${e.lastmod ?? ''}</lastmod><changefreq>${e.changefreq ?? 'weekly'}</changefreq><priority>${e.priority ?? 0.5}</priority></url>`);
|
|
126
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
127
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
128
|
+
${lines.join('\n')}
|
|
129
|
+
</urlset>`;
|
|
130
|
+
}
|
|
131
|
+
escapeXml(str) {
|
|
132
|
+
return str
|
|
133
|
+
.replace(/&/g, '&')
|
|
134
|
+
.replace(/</g, '<')
|
|
135
|
+
.replace(/>/g, '>')
|
|
136
|
+
.replace(/"/g, '"')
|
|
137
|
+
.replace(/'/g, ''');
|
|
138
|
+
}
|
|
70
139
|
async handleRequest(req, res, next) {
|
|
71
140
|
const url = req.originalUrl;
|
|
72
141
|
const pathname = url.replace(/\?.*$/, '').replace(/#.*$/, '') || '/';
|
|
@@ -100,6 +169,7 @@ class SsrServer {
|
|
|
100
169
|
}
|
|
101
170
|
let appHtml;
|
|
102
171
|
let preloadedData;
|
|
172
|
+
let helmet;
|
|
103
173
|
// Constrói um contexto simples de request (cookies raw + headers) para o getData.
|
|
104
174
|
const cookiesRaw = req.headers.cookie ?? '';
|
|
105
175
|
const cookies = {};
|
|
@@ -131,26 +201,31 @@ class SsrServer {
|
|
|
131
201
|
if (cached) {
|
|
132
202
|
appHtml = cached.html;
|
|
133
203
|
preloadedData = cached.preloadedData;
|
|
204
|
+
helmet = cached.helmet;
|
|
134
205
|
}
|
|
135
206
|
else {
|
|
136
207
|
const result = await render(url, { requestContext });
|
|
137
208
|
appHtml = typeof result.html === 'string' ? result.html : '';
|
|
138
209
|
preloadedData = result.preloadedData ?? {};
|
|
139
|
-
|
|
210
|
+
helmet = result.helmet;
|
|
211
|
+
this._ssrCache.set(cacheKey, { html: appHtml, preloadedData, helmet });
|
|
140
212
|
}
|
|
141
213
|
}
|
|
142
214
|
else {
|
|
143
215
|
const result = await render(url, { requestContext });
|
|
144
216
|
appHtml = typeof result.html === 'string' ? result.html : '';
|
|
145
217
|
preloadedData = result.preloadedData ?? {};
|
|
218
|
+
helmet = result.helmet;
|
|
146
219
|
}
|
|
147
220
|
}
|
|
148
221
|
else {
|
|
149
222
|
const result = await render(url, { requestContext });
|
|
150
223
|
appHtml = typeof result.html === 'string' ? result.html : '';
|
|
151
224
|
preloadedData = result.preloadedData ?? {};
|
|
225
|
+
helmet = result.helmet;
|
|
152
226
|
}
|
|
153
227
|
let html = template.replace('<div id="root"></div>', `<div id="root">${appHtml}</div>`);
|
|
228
|
+
html = this.injectHelmet(html, helmet);
|
|
154
229
|
html = this.injectPreloadedData(html, preloadedData);
|
|
155
230
|
if (this.vite) {
|
|
156
231
|
const transformed = await this.vite.transformIndexHtml(url, html);
|
|
@@ -186,6 +261,14 @@ class SsrServer {
|
|
|
186
261
|
return html;
|
|
187
262
|
return html.replace('</head>', this.config.cssLinkInDev);
|
|
188
263
|
}
|
|
264
|
+
injectHelmet(html, helmet) {
|
|
265
|
+
if (!helmet)
|
|
266
|
+
return html;
|
|
267
|
+
const tags = [helmet.title, helmet.meta, helmet.link, helmet.script].filter(Boolean).join('\n');
|
|
268
|
+
if (!tags)
|
|
269
|
+
return html;
|
|
270
|
+
return html.replace('</head>', `${tags}\n</head>`);
|
|
271
|
+
}
|
|
189
272
|
injectPreloadedData(html, preloadedData) {
|
|
190
273
|
if (Object.keys(preloadedData).length === 0)
|
|
191
274
|
return html;
|
package/dist/types.d.ts
CHANGED
|
@@ -3,10 +3,27 @@ import type React from 'react';
|
|
|
3
3
|
export type ComponentWithGetData = React.ComponentType<any> & {
|
|
4
4
|
getData?: (params?: RouteDataParams) => Promise<Record<string, unknown>>;
|
|
5
5
|
};
|
|
6
|
+
export type SitemapChangefreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
7
|
+
export type SitemapEntry = {
|
|
8
|
+
loc: string;
|
|
9
|
+
lastmod?: string;
|
|
10
|
+
changefreq?: SitemapChangefreq;
|
|
11
|
+
priority?: number;
|
|
12
|
+
};
|
|
13
|
+
export type SitemapRouteConfig = {
|
|
14
|
+
include: boolean;
|
|
15
|
+
changefreq?: SitemapChangefreq;
|
|
16
|
+
priority?: number;
|
|
17
|
+
/** For dynamic routes (e.g. /video/:id): return all URLs for this pattern. */
|
|
18
|
+
getEntries?: (ctx: {
|
|
19
|
+
siteUrl: string;
|
|
20
|
+
}) => Promise<SitemapEntry[]>;
|
|
21
|
+
};
|
|
6
22
|
export type RouteConfig = {
|
|
7
23
|
path: string;
|
|
8
24
|
Component: ComponentWithGetData;
|
|
9
25
|
isSSR: boolean;
|
|
26
|
+
sitemap?: SitemapRouteConfig;
|
|
10
27
|
};
|
|
11
28
|
export type RequestContext = {
|
|
12
29
|
cookiesRaw: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG;IAC5D,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1E,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG;IAC5D,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1E,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,CAAC;AAEZ,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CACpE,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,kBAAkB,CAAC;CAC9B,CAAC;AACF,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lovable-ssr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "SSR and route data engine for Lovable projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -43,10 +43,12 @@
|
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^18.0.0",
|
|
45
45
|
"react-dom": "^18.0.0",
|
|
46
|
-
"react-router-dom": "^6.0.0"
|
|
46
|
+
"react-router-dom": "^6.0.0",
|
|
47
|
+
"react-helmet-async": "^2.0.5"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
|
-
"express": "^4.21.0"
|
|
50
|
+
"express": "^4.21.0",
|
|
51
|
+
"react-helmet-async": "^2.0.5"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@types/express": "^4.17.21",
|