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.
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  /**
3
- * Wraps children with HelmetProvider and RouteDataProvider using initial data from the browser:
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;AAOvC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CA4B7E"}
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
- const { HelmetProvider } = ReactHelmetAsync;
4
+ import { SEOProvider } from './SEOContext.js';
6
5
  /**
7
- * Wraps children with HelmetProvider and RouteDataProvider using initial data from the browser:
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(HelmetProvider, { children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: initialParams, children: children }) }));
25
+ return (_jsx(SEOProvider, { children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: initialParams, children: children }) }));
27
26
  }
@@ -1,15 +1,9 @@
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
- }
1
+ import { type SEOProps } from './SEOContext.js';
2
+ export type { SEOProps };
10
3
  /**
11
- * Centralized SEO component for meta tags and JSON-LD.
12
- * Use within HelmetProvider (provided by framework at app level).
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({ title, description, image, url, type, noindex, structuredData, }: SEOProps): import("react/jsx-runtime").JSX.Element;
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":"AAIA,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"}
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"}
@@ -1,10 +1,108 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import ReactHelmetAsync from 'react-helmet-async';
3
- const { Helmet } = ReactHelmetAsync;
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 component for meta tags and JSON-LD.
6
- * Use within HelmetProvider (provided by framework at app level).
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({ title, description, image, url, type = 'website', noindex = false, structuredData, }) {
9
- 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) }))] }));
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,8 @@
1
+ import type { SEOProps } from '../components/SEOContext.js';
2
+ export declare function buildHeadHtmlFromSEO(props: SEOProps): {
3
+ title: string;
4
+ meta: string;
5
+ link: string;
6
+ script: string;
7
+ };
8
+ //# sourceMappingURL=buildHeadHtml.d.ts.map
@@ -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, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&#39;');
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
+ }
@@ -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;AAUvC,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"}
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"}
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { renderToString } from 'react-dom/server';
3
- import ReactHelmetAsync from 'react-helmet-async';
4
- import { StaticRouter } from 'react-router-dom/server';
5
- const { HelmetProvider } = ReactHelmetAsync;
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 helmetContext = {};
38
- const inner = (_jsx(HelmetProvider, { context: helmetContext, children: _jsx(StaticRouter, { location: url, children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: params, children: _jsx(AppRoutes, {}) }) }) }));
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 = helmetContext.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.20",
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",