lovable-ssr 0.1.20 → 0.1.22
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/dist/components/BrowserRouteDataProvider.d.ts +1 -1
- package/dist/components/BrowserRouteDataProvider.d.ts.map +1 -1
- package/dist/components/BrowserRouteDataProvider.js +3 -4
- package/dist/components/SEO.d.ts +6 -12
- package/dist/components/SEO.d.ts.map +1 -1
- package/dist/components/SEO.js +105 -7
- package/dist/components/SEOContext.d.ts +22 -0
- package/dist/components/SEOContext.d.ts.map +1 -0
- package/dist/components/SEOContext.js +13 -0
- package/dist/ssr/buildHeadHtml.d.ts +8 -0
- package/dist/ssr/buildHeadHtml.d.ts.map +1 -0
- package/dist/ssr/buildHeadHtml.js +32 -0
- package/dist/ssr/render.d.ts.map +1 -1
- package/dist/ssr/render.js +7 -12
- package/package.json +2 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* Wraps children with
|
|
3
|
+
* Wraps children with SEOProvider 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,10 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import ReactHelmetAsync from 'react-helmet-async';
|
|
3
2
|
import RouterService from '../router/RouterService.js';
|
|
4
3
|
import { RouteDataProvider } from '../router/RouteDataContext.js';
|
|
5
|
-
|
|
4
|
+
import { SEOProvider } from './SEOContext.js';
|
|
6
5
|
/**
|
|
7
|
-
* Wraps children with
|
|
6
|
+
* Wraps children with SEOProvider and RouteDataProvider using initial data from the browser:
|
|
8
7
|
* - window.__PRELOADED_DATA__ (from SSR)
|
|
9
8
|
* - window.location.pathname + RouterService for matchedRoute and routeParams
|
|
10
9
|
*
|
|
@@ -23,5 +22,5 @@ export function BrowserRouteDataProvider({ children }) {
|
|
|
23
22
|
routeParams: routeParamsResult.routeParams,
|
|
24
23
|
searchParams: Object.keys(searchParams).length > 0 ? searchParams : routeParamsResult.searchParams,
|
|
25
24
|
};
|
|
26
|
-
return (_jsx(
|
|
25
|
+
return (_jsx(SEOProvider, { children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: initialParams, children: children }) }));
|
|
27
26
|
}
|
package/dist/components/SEO.d.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
description: string;
|
|
4
|
-
image?: string;
|
|
5
|
-
url?: string;
|
|
6
|
-
type?: string;
|
|
7
|
-
noindex?: boolean;
|
|
8
|
-
structuredData?: object;
|
|
9
|
-
}
|
|
1
|
+
import { type SEOProps } from './SEOContext.js';
|
|
2
|
+
export type { SEOProps };
|
|
10
3
|
/**
|
|
11
|
-
* Centralized SEO
|
|
12
|
-
*
|
|
4
|
+
* Centralized SEO: meta tags and JSON-LD.
|
|
5
|
+
* SSR: reports to context for head injection.
|
|
6
|
+
* Client: updates document.title and meta via useEffect.
|
|
13
7
|
*/
|
|
14
|
-
export declare function SEO(
|
|
8
|
+
export declare function SEO(props: SEOProps): null;
|
|
15
9
|
//# sourceMappingURL=SEO.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SEO.d.ts","sourceRoot":"","sources":["../../src/components/SEO.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SEO.d.ts","sourceRoot":"","sources":["../../src/components/SEO.tsx"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE/D,YAAY,EAAE,QAAQ,EAAE,CAAC;AA4FzB;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,QAiBlC"}
|
package/dist/components/SEO.js
CHANGED
|
@@ -1,10 +1,108 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useSEOContext } from './SEOContext.js';
|
|
3
|
+
function upsertMeta(doc, selector, attrs) {
|
|
4
|
+
let el = doc.querySelector(selector);
|
|
5
|
+
if (!el) {
|
|
6
|
+
el = doc.createElement('meta');
|
|
7
|
+
doc.head.appendChild(el);
|
|
8
|
+
}
|
|
9
|
+
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
|
|
10
|
+
}
|
|
11
|
+
function applySEODOM(props) {
|
|
12
|
+
if (typeof document === 'undefined')
|
|
13
|
+
return;
|
|
14
|
+
document.title = props.title;
|
|
15
|
+
upsertMeta(document, 'meta[name="description"]', {
|
|
16
|
+
name: 'description',
|
|
17
|
+
content: props.description,
|
|
18
|
+
});
|
|
19
|
+
if (props.noindex) {
|
|
20
|
+
upsertMeta(document, 'meta[name="robots"]', {
|
|
21
|
+
name: 'robots',
|
|
22
|
+
content: 'noindex, nofollow',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (props.url) {
|
|
26
|
+
let canon = document.querySelector('link[rel="canonical"]');
|
|
27
|
+
if (!canon) {
|
|
28
|
+
canon = document.createElement('link');
|
|
29
|
+
canon.setAttribute('rel', 'canonical');
|
|
30
|
+
document.head.appendChild(canon);
|
|
31
|
+
}
|
|
32
|
+
canon.setAttribute('href', props.url);
|
|
33
|
+
}
|
|
34
|
+
upsertMeta(document, 'meta[property="og:title"]', {
|
|
35
|
+
property: 'og:title',
|
|
36
|
+
content: props.title,
|
|
37
|
+
});
|
|
38
|
+
upsertMeta(document, 'meta[property="og:description"]', {
|
|
39
|
+
property: 'og:description',
|
|
40
|
+
content: props.description,
|
|
41
|
+
});
|
|
42
|
+
upsertMeta(document, 'meta[property="og:type"]', {
|
|
43
|
+
property: 'og:type',
|
|
44
|
+
content: props.type ?? 'website',
|
|
45
|
+
});
|
|
46
|
+
if (props.url) {
|
|
47
|
+
upsertMeta(document, 'meta[property="og:url"]', {
|
|
48
|
+
property: 'og:url',
|
|
49
|
+
content: props.url,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (props.image) {
|
|
53
|
+
upsertMeta(document, 'meta[property="og:image"]', {
|
|
54
|
+
property: 'og:image',
|
|
55
|
+
content: props.image,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
upsertMeta(document, 'meta[name="twitter:card"]', {
|
|
59
|
+
name: 'twitter:card',
|
|
60
|
+
content: 'summary_large_image',
|
|
61
|
+
});
|
|
62
|
+
upsertMeta(document, 'meta[name="twitter:title"]', {
|
|
63
|
+
name: 'twitter:title',
|
|
64
|
+
content: props.title,
|
|
65
|
+
});
|
|
66
|
+
upsertMeta(document, 'meta[name="twitter:description"]', {
|
|
67
|
+
name: 'twitter:description',
|
|
68
|
+
content: props.description,
|
|
69
|
+
});
|
|
70
|
+
if (props.image) {
|
|
71
|
+
upsertMeta(document, 'meta[name="twitter:image"]', {
|
|
72
|
+
name: 'twitter:image',
|
|
73
|
+
content: props.image,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (props.structuredData) {
|
|
77
|
+
let script = document.querySelector('script[data-seo-jsonld]');
|
|
78
|
+
if (!script) {
|
|
79
|
+
script = document.createElement('script');
|
|
80
|
+
script.setAttribute('type', 'application/ld+json');
|
|
81
|
+
script.setAttribute('data-seo-jsonld', '');
|
|
82
|
+
document.head.appendChild(script);
|
|
83
|
+
}
|
|
84
|
+
script.textContent = JSON.stringify(props.structuredData);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
4
87
|
/**
|
|
5
|
-
* Centralized SEO
|
|
6
|
-
*
|
|
88
|
+
* Centralized SEO: meta tags and JSON-LD.
|
|
89
|
+
* SSR: reports to context for head injection.
|
|
90
|
+
* Client: updates document.title and meta via useEffect.
|
|
7
91
|
*/
|
|
8
|
-
export function SEO(
|
|
9
|
-
|
|
92
|
+
export function SEO(props) {
|
|
93
|
+
const ctx = useSEOContext();
|
|
94
|
+
if (ctx)
|
|
95
|
+
ctx.setMeta(props);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
applySEODOM(props);
|
|
98
|
+
}, [
|
|
99
|
+
props.title,
|
|
100
|
+
props.description,
|
|
101
|
+
props.image,
|
|
102
|
+
props.url,
|
|
103
|
+
props.type,
|
|
104
|
+
props.noindex,
|
|
105
|
+
props.structuredData ? JSON.stringify(props.structuredData) : null,
|
|
106
|
+
]);
|
|
107
|
+
return null;
|
|
10
108
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface SEOProps {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
image?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
noindex?: boolean;
|
|
9
|
+
structuredData?: object;
|
|
10
|
+
}
|
|
11
|
+
type SEOContextValue = {
|
|
12
|
+
setMeta: (meta: SEOProps) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function useSEOContext(): SEOContextValue | null;
|
|
15
|
+
export declare function SEOProvider({ children, captureRef, }: {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
captureRef?: {
|
|
18
|
+
current: SEOProps | null;
|
|
19
|
+
};
|
|
20
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=SEOContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SEOContext.d.ts","sourceRoot":"","sources":["../../src/components/SEOContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,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,KAAK,eAAe,GAAG;IACrB,OAAO,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;CACnC,CAAC;AAIF,wBAAgB,aAAa,IAAI,eAAe,GAAG,IAAI,CAEtD;AAED,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,UAAU,GACX,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAA;KAAE,CAAC;CAC3C,2CASA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
const SEOContext = createContext(null);
|
|
4
|
+
export function useSEOContext() {
|
|
5
|
+
return useContext(SEOContext);
|
|
6
|
+
}
|
|
7
|
+
export function SEOProvider({ children, captureRef, }) {
|
|
8
|
+
const setMeta = (meta) => {
|
|
9
|
+
if (captureRef)
|
|
10
|
+
captureRef.current = meta;
|
|
11
|
+
};
|
|
12
|
+
return (_jsx(SEOContext.Provider, { value: { setMeta }, children: children }));
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildHeadHtml.d.ts","sourceRoot":"","sources":["../../src/ssr/buildHeadHtml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAW5D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,GAAG;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAwBA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function escapeHtml(s) {
|
|
2
|
+
return s
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
}
|
|
9
|
+
export function buildHeadHtmlFromSEO(props) {
|
|
10
|
+
const title = `<title>${escapeHtml(props.title)}</title>`;
|
|
11
|
+
const metaTags = [
|
|
12
|
+
`<meta name="description" content="${escapeHtml(props.description)}">`,
|
|
13
|
+
props.noindex ? '<meta name="robots" content="noindex, nofollow">' : null,
|
|
14
|
+
`<meta property="og:title" content="${escapeHtml(props.title)}">`,
|
|
15
|
+
`<meta property="og:description" content="${escapeHtml(props.description)}">`,
|
|
16
|
+
`<meta property="og:type" content="${escapeHtml(props.type ?? 'website')}">`,
|
|
17
|
+
props.url ? `<meta property="og:url" content="${escapeHtml(props.url)}">` : null,
|
|
18
|
+
props.image ? `<meta property="og:image" content="${escapeHtml(props.image)}">` : null,
|
|
19
|
+
'<meta name="twitter:card" content="summary_large_image">',
|
|
20
|
+
`<meta name="twitter:title" content="${escapeHtml(props.title)}">`,
|
|
21
|
+
`<meta name="twitter:description" content="${escapeHtml(props.description)}">`,
|
|
22
|
+
props.image ? `<meta name="twitter:image" content="${escapeHtml(props.image)}">` : null,
|
|
23
|
+
].filter((t) => t != null);
|
|
24
|
+
const meta = metaTags.join('\n');
|
|
25
|
+
const link = props.url
|
|
26
|
+
? `<link rel="canonical" href="${escapeHtml(props.url)}">`
|
|
27
|
+
: '';
|
|
28
|
+
const script = props.structuredData != null
|
|
29
|
+
? `<script type="application/ld+json">${JSON.stringify(props.structuredData)}</script>`
|
|
30
|
+
: '';
|
|
31
|
+
return { title, meta, link, script };
|
|
32
|
+
}
|
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;AASvC,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,CAgDxF"}
|
package/dist/ssr/render.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { renderToString } from 'react-dom/server';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { StaticRouter } from 'react-router-dom/server.js';
|
|
4
|
+
import { SEOProvider } from '../components/SEOContext.js';
|
|
5
|
+
import { buildHeadHtmlFromSEO } from './buildHeadHtml.js';
|
|
6
6
|
import { AppRoutes } from '../components/AppRoutes.js';
|
|
7
7
|
import { RouteDataProvider } from '../router/RouteDataContext.js';
|
|
8
8
|
import RouterService from '../router/RouterService.js';
|
|
@@ -34,17 +34,12 @@ export async function render(url, options) {
|
|
|
34
34
|
preloadedData = { ...preloadedData, is_success: false };
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
const
|
|
38
|
-
const inner = (_jsx(
|
|
37
|
+
const seoCapture = { current: null };
|
|
38
|
+
const inner = (_jsx(SEOProvider, { captureRef: seoCapture, children: _jsx(StaticRouter, { location: url, children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: params, children: _jsx(AppRoutes, {}) }) }) }));
|
|
39
39
|
const app = options?.wrap ? options.wrap(inner) : inner;
|
|
40
40
|
const html = renderToString(app);
|
|
41
|
-
const helmet =
|
|
42
|
-
?
|
|
43
|
-
title: helmetContext.helmet.title.toString(),
|
|
44
|
-
meta: helmetContext.helmet.meta.toString(),
|
|
45
|
-
link: helmetContext.helmet.link.toString(),
|
|
46
|
-
script: helmetContext.helmet.script.toString(),
|
|
47
|
-
}
|
|
41
|
+
const helmet = seoCapture.current
|
|
42
|
+
? buildHeadHtmlFromSEO(seoCapture.current)
|
|
48
43
|
: undefined;
|
|
49
44
|
return { html, preloadedData, helmet };
|
|
50
45
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lovable-ssr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "SSR and route data engine for Lovable projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -43,12 +43,10 @@
|
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^18.0.0",
|
|
45
45
|
"react-dom": "^18.0.0",
|
|
46
|
-
"react-helmet-async": "^2.0.5",
|
|
47
46
|
"react-router-dom": "^6.0.0"
|
|
48
47
|
},
|
|
49
48
|
"dependencies": {
|
|
50
|
-
"express": "^4.21.0"
|
|
51
|
-
"react-helmet-async": "^2.0.5"
|
|
49
|
+
"express": "^4.21.0"
|
|
52
50
|
},
|
|
53
51
|
"devDependencies": {
|
|
54
52
|
"@types/express": "^4.17.21",
|