@dreamtree-org/landing-kit 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/MIGRATION.md +87 -0
- package/README.md +191 -2
- package/auth.css +195 -0
- package/auth.d.ts +2 -0
- package/dist/auth.cjs +232 -0
- package/dist/auth.js +211 -0
- package/dist/enhance.cjs +83 -0
- package/dist/enhance.js +63 -0
- package/dist/index.cjs +1138 -0
- package/dist/index.js +1117 -0
- package/dist/seo.cjs +343 -0
- package/dist/seo.js +305 -0
- package/dist/server.cjs +569 -0
- package/dist/server.js +512 -0
- package/enhance.d.ts +12 -0
- package/index.d.ts +224 -0
- package/package.json +94 -4
- package/seo.d.ts +50 -0
- package/server.d.ts +43 -0
- package/styles.css +836 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// src/server.js
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/seo.js
|
|
6
|
+
var SITE_PLACEHOLDER = "__SITE_URL__";
|
|
7
|
+
var AI_CRAWLERS = [
|
|
8
|
+
"GPTBot",
|
|
9
|
+
"OAI-SearchBot",
|
|
10
|
+
"ChatGPT-User",
|
|
11
|
+
"ClaudeBot",
|
|
12
|
+
"Claude-SearchBot",
|
|
13
|
+
"Claude-User",
|
|
14
|
+
"PerplexityBot",
|
|
15
|
+
"Perplexity-User",
|
|
16
|
+
"Google-Extended",
|
|
17
|
+
"Applebot-Extended",
|
|
18
|
+
"Bingbot",
|
|
19
|
+
"CCBot"
|
|
20
|
+
];
|
|
21
|
+
var DEFAULT_PRIVATE_PATHS = ["/api/", "/oauth/", "/.well-known/", "/mcp"];
|
|
22
|
+
var PRIVATE_ROBOTS = "User-agent: *\nDisallow: /\n";
|
|
23
|
+
var DEFAULT_LOCALE = { lang: "en-IN", og: "en_IN", country: "India" };
|
|
24
|
+
var DEFAULT_IMAGE = { path: "/og-image.png", width: 1200, height: 630, type: "image/png" };
|
|
25
|
+
function escapeHtml(value) {
|
|
26
|
+
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
27
|
+
}
|
|
28
|
+
function jsonLdScript(graph) {
|
|
29
|
+
return JSON.stringify(graph).replace(/</g, "\\u003c");
|
|
30
|
+
}
|
|
31
|
+
function pageUrl(path2 = "/", site = SITE_PLACEHOLDER) {
|
|
32
|
+
if (/^https?:\/\//.test(path2)) return path2;
|
|
33
|
+
if (site == null) return void 0;
|
|
34
|
+
const base = String(site).replace(/\/+$/, "");
|
|
35
|
+
return path2 === "/" ? `${base}/` : `${base}${path2.startsWith("/") ? path2 : `/${path2}`}`;
|
|
36
|
+
}
|
|
37
|
+
var imageOf = (product) => {
|
|
38
|
+
const img = product.image;
|
|
39
|
+
if (!img) return DEFAULT_IMAGE;
|
|
40
|
+
if (typeof img === "string") return { ...DEFAULT_IMAGE, path: img, type: /\.png$/i.test(img) ? "image/png" : void 0 };
|
|
41
|
+
return { ...DEFAULT_IMAGE, ...img };
|
|
42
|
+
};
|
|
43
|
+
var need = (fn, obj, keys) => {
|
|
44
|
+
for (const k of keys) {
|
|
45
|
+
const v = k.split(".").reduce((o, p) => o == null ? o : o[p], obj);
|
|
46
|
+
if (v == null || v === "") throw new Error(`${fn}: "${k}" is missing \u2014 pass it in the ${k.split(".")[0]} object.`);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function buildJsonLd({
|
|
50
|
+
site = SITE_PLACEHOLDER,
|
|
51
|
+
product,
|
|
52
|
+
org = { name: "Dreamtree-Org" },
|
|
53
|
+
page = { path: "/" },
|
|
54
|
+
faq,
|
|
55
|
+
howTo,
|
|
56
|
+
breadcrumbs,
|
|
57
|
+
parts,
|
|
58
|
+
keywords,
|
|
59
|
+
lastmod,
|
|
60
|
+
speakable,
|
|
61
|
+
locale = DEFAULT_LOCALE
|
|
62
|
+
}) {
|
|
63
|
+
need("buildJsonLd", { product, page }, ["product.name", "product.definition"]);
|
|
64
|
+
const u = (p) => pageUrl(p, site);
|
|
65
|
+
const home = u("/");
|
|
66
|
+
const id = (frag, base = home) => base ? `${base}#${frag}` : void 0;
|
|
67
|
+
const ref = (frag) => home ? { "@id": id(frag) } : void 0;
|
|
68
|
+
const url = u(page.path || "/");
|
|
69
|
+
const image = imageOf(product);
|
|
70
|
+
const imageUrl = u(image.path);
|
|
71
|
+
const sameAs = (org.sameAs || []).filter((s) => /^https?:\/\//.test(s));
|
|
72
|
+
const graph = [
|
|
73
|
+
{
|
|
74
|
+
"@type": "Organization",
|
|
75
|
+
"@id": id("organization"),
|
|
76
|
+
name: org.name,
|
|
77
|
+
url: home,
|
|
78
|
+
...org.logo ? { logo: { "@type": "ImageObject", url: u(org.logo) } } : {},
|
|
79
|
+
...product.logo ? { brand: { "@type": "Brand", name: product.name, logo: u(product.logo) } } : { brand: { "@type": "Brand", name: product.name } },
|
|
80
|
+
...org.email ? { email: org.email, contactPoint: { "@type": "ContactPoint", contactType: "sales", email: org.email } } : {},
|
|
81
|
+
...sameAs.length ? { sameAs } : {}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"@type": "WebSite",
|
|
85
|
+
"@id": id("website"),
|
|
86
|
+
url: home,
|
|
87
|
+
name: product.name,
|
|
88
|
+
...product.description ? { description: product.description } : {},
|
|
89
|
+
publisher: ref("organization"),
|
|
90
|
+
inLanguage: locale.lang
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"@type": "SoftwareApplication",
|
|
94
|
+
"@id": id("software"),
|
|
95
|
+
name: product.name,
|
|
96
|
+
description: product.definition,
|
|
97
|
+
applicationCategory: product.category || "BusinessApplication",
|
|
98
|
+
...product.subCategory ? { applicationSubCategory: product.subCategory } : {},
|
|
99
|
+
operatingSystem: product.operatingSystem || "Web browser",
|
|
100
|
+
url: home,
|
|
101
|
+
image: imageUrl,
|
|
102
|
+
...product.screenshot ? { screenshot: u(product.screenshot) } : {},
|
|
103
|
+
...product.features && product.features.length ? { featureList: product.features } : {},
|
|
104
|
+
...product.audience ? { audience: { "@type": "BusinessAudience", audienceType: product.audience } } : {},
|
|
105
|
+
...locale.country ? { areaServed: { "@type": "Country", name: locale.country } } : {},
|
|
106
|
+
publisher: ref("organization"),
|
|
107
|
+
inLanguage: locale.lang,
|
|
108
|
+
...product.offers ? {
|
|
109
|
+
offers: {
|
|
110
|
+
"@type": "Offer",
|
|
111
|
+
description: product.offers.description || "Contact us for pricing",
|
|
112
|
+
url: u(product.offers.url || "/#book-demo"),
|
|
113
|
+
availability: "https://schema.org/InStock",
|
|
114
|
+
seller: ref("organization")
|
|
115
|
+
}
|
|
116
|
+
} : {}
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
if (page.title) {
|
|
120
|
+
graph.push({
|
|
121
|
+
"@type": "WebPage",
|
|
122
|
+
"@id": id("webpage", url),
|
|
123
|
+
url,
|
|
124
|
+
name: page.title,
|
|
125
|
+
...page.description ? { description: page.description } : {},
|
|
126
|
+
isPartOf: ref("website"),
|
|
127
|
+
about: ref("software"),
|
|
128
|
+
primaryImageOfPage: { "@type": "ImageObject", url: imageUrl, width: image.width, height: image.height },
|
|
129
|
+
inLanguage: locale.lang,
|
|
130
|
+
...lastmod ? { dateModified: lastmod } : {},
|
|
131
|
+
...speakable && speakable.length ? { speakable: { "@type": "SpeakableSpecification", cssSelector: speakable } } : {},
|
|
132
|
+
...url ? { breadcrumb: { "@id": id("breadcrumb", url) } } : {},
|
|
133
|
+
...keywords && keywords.length ? { keywords: keywords.join(", ") } : {},
|
|
134
|
+
...parts && parts.length ? { hasPart: parts.map((p) => ({ "@type": "WebPage", url: u(p.path), name: p.name })) } : {}
|
|
135
|
+
});
|
|
136
|
+
const trail = breadcrumbs || (page.path && page.path !== "/" ? [[product.name, "/"], [page.name || page.title, page.path]] : [[product.name, "/"]]);
|
|
137
|
+
graph.push({
|
|
138
|
+
"@type": "BreadcrumbList",
|
|
139
|
+
"@id": id("breadcrumb", url),
|
|
140
|
+
itemListElement: trail.map(([name, p], i) => ({ "@type": "ListItem", position: i + 1, name, item: u(p) }))
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (faq && faq.length) {
|
|
144
|
+
graph.push({
|
|
145
|
+
"@type": "FAQPage",
|
|
146
|
+
"@id": id("faq", url),
|
|
147
|
+
...page.title && url ? { isPartOf: { "@id": id("webpage", url) } } : {},
|
|
148
|
+
mainEntity: faq.map((f) => ({ "@type": "Question", name: f.q, acceptedAnswer: { "@type": "Answer", text: f.a } }))
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (howTo && howTo.steps && howTo.steps.length) {
|
|
152
|
+
const anchor = howTo.anchor || "how-it-works";
|
|
153
|
+
graph.push({
|
|
154
|
+
"@type": "HowTo",
|
|
155
|
+
"@id": id(anchor, url),
|
|
156
|
+
name: howTo.name || `How to use ${product.name}`,
|
|
157
|
+
...howTo.description ? { description: howTo.description } : {},
|
|
158
|
+
tool: { "@type": "HowToTool", name: product.name },
|
|
159
|
+
step: howTo.steps.map((st, i) => ({
|
|
160
|
+
"@type": "HowToStep",
|
|
161
|
+
position: i + 1,
|
|
162
|
+
name: st.name || st.title,
|
|
163
|
+
text: st.text || st.body,
|
|
164
|
+
...url ? { url: `${url}#${anchor}` } : {}
|
|
165
|
+
}))
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return { "@context": "https://schema.org", "@graph": JSON.parse(JSON.stringify(graph)) };
|
|
169
|
+
}
|
|
170
|
+
function contentFacts(content = {}) {
|
|
171
|
+
const steps = content.steps ? content.steps.steps || content.steps.items : null;
|
|
172
|
+
const sections = [];
|
|
173
|
+
for (const s of content.sections || []) {
|
|
174
|
+
if (!s || !s.title || s.use) continue;
|
|
175
|
+
const items = (s.items || []).map((i) => typeof i === "string" ? `- ${i}` : `- ${i.title}${i.body ? `: ${i.body}` : ""}`);
|
|
176
|
+
const extra = [s.detail, s.note].filter(Boolean).join(" ");
|
|
177
|
+
sections.push([s.eyebrow || s.title, [s.body, extra, items.join("\n")].filter(Boolean).join("\n\n")]);
|
|
178
|
+
}
|
|
179
|
+
if (content.audiences) {
|
|
180
|
+
sections.push([content.audiences.title, (content.audiences.items || []).map((a) => `- ${a.title}: ${a.body}`).join("\n")]);
|
|
181
|
+
}
|
|
182
|
+
if (steps) sections.push([content.steps.title, steps.map((st, i) => `${i + 1}. ${st.name || st.title}: ${st.text || st.body}`).join("\n")]);
|
|
183
|
+
return {
|
|
184
|
+
faq: content.faq ? content.faq.items || [] : [],
|
|
185
|
+
howTo: steps ? { name: content.steps.howToName, description: content.steps.title, steps, anchor: content.steps.id || "how-it-works" } : null,
|
|
186
|
+
features: content.product && content.product.features || [],
|
|
187
|
+
sections
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function buildHead({ site = SITE_PLACEHOLDER, page, product, twitterHandle, jsonLd, locale = DEFAULT_LOCALE, robots }) {
|
|
191
|
+
need("buildHead", { page, product }, ["page.title", "page.description", "product.name"]);
|
|
192
|
+
const e = escapeHtml;
|
|
193
|
+
const url = pageUrl(page.path || "/", site);
|
|
194
|
+
const image = imageOf(product);
|
|
195
|
+
const imageUrl = pageUrl(image.path, site);
|
|
196
|
+
const alt = image.alt || `${product.name}: ${page.name && page.name !== "Home" ? page.name : page.title}`;
|
|
197
|
+
const handle = twitterHandle ? `@${String(twitterHandle).replace(/^@/, "")}` : "";
|
|
198
|
+
return [
|
|
199
|
+
`<title>${e(page.title)}</title>`,
|
|
200
|
+
`<meta name="description" content="${e(page.description)}" />`,
|
|
201
|
+
`<meta name="robots" content="${e(robots || "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1")}" />`,
|
|
202
|
+
url ? `<link rel="canonical" href="${e(url)}" />` : null,
|
|
203
|
+
url ? `<link rel="alternate" hreflang="${locale.lang}" href="${e(url)}" />` : null,
|
|
204
|
+
url ? `<link rel="alternate" hreflang="x-default" href="${e(url)}" />` : null,
|
|
205
|
+
'<meta property="og:type" content="website" />',
|
|
206
|
+
`<meta property="og:site_name" content="${e(product.name)}" />`,
|
|
207
|
+
`<meta property="og:locale" content="${locale.og}" />`,
|
|
208
|
+
url ? `<meta property="og:url" content="${e(url)}" />` : null,
|
|
209
|
+
`<meta property="og:title" content="${e(page.title)}" />`,
|
|
210
|
+
`<meta property="og:description" content="${e(page.description)}" />`,
|
|
211
|
+
imageUrl ? `<meta property="og:image" content="${e(imageUrl)}" />` : null,
|
|
212
|
+
image.type ? `<meta property="og:image:type" content="${e(image.type)}" />` : null,
|
|
213
|
+
`<meta property="og:image:width" content="${image.width}" />`,
|
|
214
|
+
`<meta property="og:image:height" content="${image.height}" />`,
|
|
215
|
+
`<meta property="og:image:alt" content="${e(alt)}" />`,
|
|
216
|
+
'<meta name="twitter:card" content="summary_large_image" />',
|
|
217
|
+
handle ? `<meta name="twitter:site" content="${e(handle)}" />` : null,
|
|
218
|
+
`<meta name="twitter:title" content="${e(page.title)}" />`,
|
|
219
|
+
`<meta name="twitter:description" content="${e(page.description)}" />`,
|
|
220
|
+
imageUrl ? `<meta name="twitter:image" content="${e(imageUrl)}" />` : null,
|
|
221
|
+
site != null ? `<link rel="alternate" type="text/plain" title="LLM summary" href="${e(pageUrl("/llms.txt", site))}" />` : null,
|
|
222
|
+
jsonLd ? `<script type="application/ld+json">${jsonLdScript(jsonLd)}</script>` : null
|
|
223
|
+
].filter(Boolean).join("\n ");
|
|
224
|
+
}
|
|
225
|
+
function buildRobots({ site = SITE_PLACEHOLDER, privatePaths = DEFAULT_PRIVATE_PATHS, crawlers = AI_CRAWLERS } = {}) {
|
|
226
|
+
const rules = ["Allow: /", ...privatePaths.map((p) => `Disallow: ${p}`)];
|
|
227
|
+
const blocks = ["*", ...crawlers].map((agent) => [`User-agent: ${agent}`, ...rules].join("\n"));
|
|
228
|
+
const map = pageUrl("/sitemap.xml", site);
|
|
229
|
+
return `${blocks.join("\n\n")}
|
|
230
|
+
${map ? `
|
|
231
|
+
Sitemap: ${map}
|
|
232
|
+
` : ""}`;
|
|
233
|
+
}
|
|
234
|
+
function buildSitemap({ site = SITE_PLACEHOLDER, pages, lastmod, changefreq = "monthly" }) {
|
|
235
|
+
if (!Array.isArray(pages) || !pages.length) throw new Error('buildSitemap: "pages" is missing \u2014 pass [{ path: "/" }, ...].');
|
|
236
|
+
return [
|
|
237
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
238
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
239
|
+
...pages.map((p) => {
|
|
240
|
+
const pr = p.priority || (p.path === "/" ? "1.0" : "0.8");
|
|
241
|
+
return ` <url><loc>${escapeHtml(pageUrl(p.path, site))}</loc>${lastmod ? `<lastmod>${lastmod}</lastmod>` : ""}<changefreq>${p.changefreq || changefreq}</changefreq><priority>${pr}</priority></url>`;
|
|
242
|
+
}),
|
|
243
|
+
"</urlset>",
|
|
244
|
+
""
|
|
245
|
+
].join("\n");
|
|
246
|
+
}
|
|
247
|
+
function buildLlmsTxt({ site = SITE_PLACEHOLDER, product, org = { name: "Dreamtree-Org" }, pages = [], about, facts, optional }) {
|
|
248
|
+
need("buildLlmsTxt", { product }, ["product.name", "product.definition"]);
|
|
249
|
+
const u = (p) => pageUrl(p, site);
|
|
250
|
+
return [
|
|
251
|
+
`# ${product.name}`,
|
|
252
|
+
"",
|
|
253
|
+
`> ${product.definition}`,
|
|
254
|
+
"",
|
|
255
|
+
about || `${product.name} is made by ${org.name}. It runs in the web browser.`,
|
|
256
|
+
"",
|
|
257
|
+
"## Pages",
|
|
258
|
+
"",
|
|
259
|
+
...pages.map((p) => `- [${p.path === "/" ? `${product.name} home` : p.name || p.title}](${u(p.path)}): ${p.summary || p.description}`),
|
|
260
|
+
`- [Full product description for language models](${u("/llms-full.txt")}): every feature, page and FAQ in plain text`,
|
|
261
|
+
"",
|
|
262
|
+
"## What it does",
|
|
263
|
+
"",
|
|
264
|
+
...(facts || product.features || []).map((f) => `- ${f}`),
|
|
265
|
+
"",
|
|
266
|
+
"## Optional",
|
|
267
|
+
"",
|
|
268
|
+
...(optional || [
|
|
269
|
+
{ label: "Book a demo", path: "/#book-demo" },
|
|
270
|
+
{ label: "Log in", path: "/login" }
|
|
271
|
+
]).map((o) => `- [${o.label}](${u(o.path)})`),
|
|
272
|
+
""
|
|
273
|
+
].join("\n");
|
|
274
|
+
}
|
|
275
|
+
function buildLlmsFullTxt({ site = SITE_PLACEHOLDER, product, org = { name: "Dreamtree-Org" }, pages = [], sections = () => [] }) {
|
|
276
|
+
need("buildLlmsFullTxt", { product }, ["product.name", "product.definition"]);
|
|
277
|
+
const out = [`# ${product.name}: full description`, "", `> ${product.definition}`, "", `Made by ${org.name}. Source: ${pageUrl("/", site)}`, ""];
|
|
278
|
+
for (const page of pages) {
|
|
279
|
+
out.push(`## ${page.path === "/" ? "Overview" : page.name || page.title}`, "", `Page: ${pageUrl(page.path, site)}`, "");
|
|
280
|
+
if (page.description) out.push(page.description, "");
|
|
281
|
+
for (const [heading, body] of sections(page) || []) {
|
|
282
|
+
out.push(`### ${heading}`, "");
|
|
283
|
+
if (body) out.push(body, "");
|
|
284
|
+
}
|
|
285
|
+
if (page.faq && page.faq.length) {
|
|
286
|
+
out.push("### Frequently asked questions", "");
|
|
287
|
+
for (const f of page.faq) out.push(`**${f.q}**`, "", f.a, "");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (product.features && product.features.length) out.push("## Features", "", ...product.features.map((f) => `- ${f}`), "");
|
|
291
|
+
return out.join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/server.js
|
|
295
|
+
var CRAWLER_FILES = {
|
|
296
|
+
"/robots.txt": "text/plain; charset=utf-8",
|
|
297
|
+
"/sitemap.xml": "application/xml; charset=utf-8",
|
|
298
|
+
"/llms.txt": "text/plain; charset=utf-8",
|
|
299
|
+
"/llms-full.txt": "text/plain; charset=utf-8"
|
|
300
|
+
};
|
|
301
|
+
var PLACEHOLDER_RE = new RegExp(SITE_PLACEHOLDER, "g");
|
|
302
|
+
var VERIFY_SLOT = "<!--site-verification-->";
|
|
303
|
+
function siteUrl(req, { appUrl = process.env.APP_URL } = {}) {
|
|
304
|
+
const configured = String(appUrl || "").trim().replace(/\/+$/, "");
|
|
305
|
+
if (configured) return configured;
|
|
306
|
+
const h = req.headers || {};
|
|
307
|
+
const proto = String(h["x-forwarded-proto"] || req.protocol || "http").split(",")[0].trim();
|
|
308
|
+
const host = String(h["x-forwarded-host"] || h.host || "localhost").split(",")[0].trim();
|
|
309
|
+
return `${proto}://${host}`;
|
|
310
|
+
}
|
|
311
|
+
function readCookie(req, name) {
|
|
312
|
+
const raw = String(req.headers && req.headers.cookie || "");
|
|
313
|
+
const m = new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]+)`).exec(raw);
|
|
314
|
+
if (!m) return null;
|
|
315
|
+
try {
|
|
316
|
+
return decodeURIComponent(m[1]);
|
|
317
|
+
} catch {
|
|
318
|
+
return m[1];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function sessionCheck({ cookie = "JWT_TOKEN", bearer = false, verify } = {}) {
|
|
322
|
+
if (typeof verify !== "function") {
|
|
323
|
+
throw new Error('landing-kit sessionCheck: "verify" is missing \u2014 pass (token) => boolean using your JWT check.');
|
|
324
|
+
}
|
|
325
|
+
return (req) => {
|
|
326
|
+
let token = cookie ? readCookie(req, cookie) : null;
|
|
327
|
+
if (!token && bearer) {
|
|
328
|
+
const auth = String(req.headers && req.headers.authorization || "");
|
|
329
|
+
if (/^Bearer\s+/i.test(auth)) token = auth.replace(/^Bearer\s+/i, "").trim();
|
|
330
|
+
}
|
|
331
|
+
if (!token) return false;
|
|
332
|
+
try {
|
|
333
|
+
return verify(token) === true;
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function verificationTags(env = process.env) {
|
|
340
|
+
const tags = [];
|
|
341
|
+
const google = String(env.GOOGLE_SITE_VERIFICATION || "").trim();
|
|
342
|
+
const bing = String(env.BING_SITE_VERIFICATION || "").trim();
|
|
343
|
+
if (google) tags.push(`<meta name="google-site-verification" content="${escapeHtml(google)}" />`);
|
|
344
|
+
if (bing) tags.push(`<meta name="msvalidate.01" content="${escapeHtml(bing)}" />`);
|
|
345
|
+
return tags.join("\n ");
|
|
346
|
+
}
|
|
347
|
+
function fillSite(body, origin, { html = false, env = process.env } = {}) {
|
|
348
|
+
const out = String(body).replace(PLACEHOLDER_RE, origin);
|
|
349
|
+
return html ? out.replace(VERIFY_SLOT, () => verificationTags(env)) : out;
|
|
350
|
+
}
|
|
351
|
+
function landingRoutes(app, options = {}) {
|
|
352
|
+
const opts = typeof options === "string" ? { dist: options } : options;
|
|
353
|
+
const {
|
|
354
|
+
dist,
|
|
355
|
+
manifest: manifestFile = "landing-pages.json",
|
|
356
|
+
isPublicHost = () => true,
|
|
357
|
+
hasSession = () => false,
|
|
358
|
+
onSession = "next",
|
|
359
|
+
onPrivateHost = "next",
|
|
360
|
+
trailingSlash = "redirect",
|
|
361
|
+
rawFiles: rawMode = "not-found",
|
|
362
|
+
redirects = {},
|
|
363
|
+
cacheControl = "public, max-age=300",
|
|
364
|
+
crawlerCache = "public, max-age=3600",
|
|
365
|
+
privateRobots = PRIVATE_ROBOTS,
|
|
366
|
+
appUrl,
|
|
367
|
+
env = process.env
|
|
368
|
+
} = opts;
|
|
369
|
+
if (!app || typeof app.get !== "function") throw new Error("landing-kit landingRoutes: pass the Express app as the first argument.");
|
|
370
|
+
if (!dist) throw new Error('landing-kit landingRoutes: "dist" is missing \u2014 pass { dist: path.join(__dirname, "client/dist") }.');
|
|
371
|
+
const cache = /* @__PURE__ */ new Map();
|
|
372
|
+
const read = (rel) => {
|
|
373
|
+
if (!cache.has(rel)) {
|
|
374
|
+
const full = path.join(dist, rel);
|
|
375
|
+
cache.set(rel, fs.existsSync(full) && fs.statSync(full).isFile() ? fs.readFileSync(full, "utf8") : null);
|
|
376
|
+
}
|
|
377
|
+
return cache.get(rel);
|
|
378
|
+
};
|
|
379
|
+
const origin = (req) => siteUrl(req, appUrl !== void 0 ? { appUrl } : void 0);
|
|
380
|
+
const base = path.posix.dirname(manifestFile.split(path.sep).join("/"));
|
|
381
|
+
const fromManifest = (f) => base === "." ? f : path.posix.join(base, f);
|
|
382
|
+
let routes = {};
|
|
383
|
+
const rawManifest = read(manifestFile);
|
|
384
|
+
if (rawManifest) {
|
|
385
|
+
try {
|
|
386
|
+
routes = Object.fromEntries(Object.entries(JSON.parse(rawManifest) || {}).map(([p, f]) => [p, fromManifest(f)]));
|
|
387
|
+
} catch {
|
|
388
|
+
routes = {};
|
|
389
|
+
}
|
|
390
|
+
} else if (read("landing.html") != null) {
|
|
391
|
+
routes = { "/": "landing.html" };
|
|
392
|
+
}
|
|
393
|
+
const isPublic = (req) => Promise.resolve(isPublicHost(req)).then(Boolean);
|
|
394
|
+
const guard = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
|
395
|
+
const appShell = (res, next) => {
|
|
396
|
+
res.set("Cache-Control", "no-cache");
|
|
397
|
+
return res.sendFile(path.join(dist, "index.html"), (err) => err && next(err));
|
|
398
|
+
};
|
|
399
|
+
const sendPage = (req, res, file) => {
|
|
400
|
+
res.set("Cache-Control", cacheControl);
|
|
401
|
+
res.type("html").send(fillSite(read(file), origin(req), { html: true, env }));
|
|
402
|
+
};
|
|
403
|
+
app.get("/", guard(async (req, res, next) => {
|
|
404
|
+
const file = routes["/"];
|
|
405
|
+
if (!file || read(file) == null) return next();
|
|
406
|
+
if (!await isPublic(req)) return onPrivateHost === "app-shell" ? appShell(res, next) : next();
|
|
407
|
+
res.set("Vary", "Cookie");
|
|
408
|
+
if (hasSession(req)) return onSession === "app-shell" ? appShell(res, next) : next();
|
|
409
|
+
sendPage(req, res, file);
|
|
410
|
+
}));
|
|
411
|
+
const byFile = Object.fromEntries(Object.entries(routes).map(([route, f]) => [`/${f}`, route]));
|
|
412
|
+
const rawSet = /* @__PURE__ */ new Set([`/${manifestFile.split(path.sep).join("/")}`, ...Object.keys(byFile)]);
|
|
413
|
+
const rawDirs = [...new Set(Object.values(routes).filter((f) => f.includes("/")).map((f) => `/${f.split("/")[0]}/`))];
|
|
414
|
+
app.use((req, res, next) => {
|
|
415
|
+
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
|
416
|
+
const p = req.path;
|
|
417
|
+
if (!rawSet.has(p) && !rawDirs.some((d) => p.startsWith(d))) return next();
|
|
418
|
+
if (rawMode === "redirect" && byFile[p]) return res.redirect(301, byFile[p]);
|
|
419
|
+
return res.status(404).type("text/plain").send("Not found");
|
|
420
|
+
});
|
|
421
|
+
for (const [from, to] of Object.entries(redirects)) {
|
|
422
|
+
app.get(from, guard(async (req, res, next) => await isPublic(req) ? res.redirect(301, to) : next()));
|
|
423
|
+
}
|
|
424
|
+
const others = Object.keys(routes).filter((p) => p !== "/");
|
|
425
|
+
if (others.length) {
|
|
426
|
+
app.get(
|
|
427
|
+
others.flatMap((p) => [p, `${p}/`]),
|
|
428
|
+
guard(async (req, res, next) => {
|
|
429
|
+
if (!await isPublic(req)) return next();
|
|
430
|
+
const trimmed = req.path.replace(/\/+$/, "");
|
|
431
|
+
const file = routes[trimmed];
|
|
432
|
+
if (!file || read(file) == null) return next();
|
|
433
|
+
if (trimmed !== req.path && trailingSlash !== "serve") return res.redirect(301, trimmed + req.originalUrl.slice(req.path.length));
|
|
434
|
+
sendPage(req, res, file);
|
|
435
|
+
})
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
for (const [route, type] of Object.entries(CRAWLER_FILES)) {
|
|
439
|
+
app.get(route, guard(async (req, res, next) => {
|
|
440
|
+
const body = await isPublic(req) ? read(route.slice(1)) : null;
|
|
441
|
+
if (body == null) {
|
|
442
|
+
if (route !== "/robots.txt") return next();
|
|
443
|
+
return res.type("text/plain").send(privateRobots);
|
|
444
|
+
}
|
|
445
|
+
res.set("Cache-Control", crawlerCache);
|
|
446
|
+
res.type(type).send(fillSite(body, origin(req)));
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
return Object.keys(routes);
|
|
450
|
+
}
|
|
451
|
+
function pageFile(p, { homeFile = "landing.html", pagesDir = "landing-pages" } = {}) {
|
|
452
|
+
return p === "/" ? homeFile : path.posix.join(pagesDir, `${p.replace(/^\/+|\/+$/g, "")}.html`);
|
|
453
|
+
}
|
|
454
|
+
function prerenderPages({ dist, template = "landing.html", pages, files = {}, homeFile = "landing.html", pagesDir = "landing-pages", manifest = "landing-pages.json", slot = "<!--landing-->" }) {
|
|
455
|
+
if (!dist) throw new Error('landing-kit prerenderPages: "dist" is missing.');
|
|
456
|
+
if (!Array.isArray(pages) || !pages.length) throw new Error('landing-kit prerenderPages: "pages" is missing \u2014 pass [{ path, html, title, description }].');
|
|
457
|
+
const tpl = template.includes("<") ? template : fs.readFileSync(path.join(dist, template), "utf8");
|
|
458
|
+
if (!tpl.includes(slot)) throw new Error(`landing-kit prerenderPages: the template has no ${slot} slot for the page body.`);
|
|
459
|
+
const esc = escapeHtml;
|
|
460
|
+
const outputs = [];
|
|
461
|
+
for (const p of pages) {
|
|
462
|
+
for (const k of ["path", "html"]) if (p[k] == null) throw new Error(`landing-kit prerenderPages: page "${p.path || "?"}" has no "${k}".`);
|
|
463
|
+
const jsonLd = p.jsonLd == null ? "" : typeof p.jsonLd === "string" ? p.jsonLd : JSON.stringify(p.jsonLd).replace(/</g, "\\u003c");
|
|
464
|
+
const out = tpl.replace(/%TITLE%/g, () => esc(p.title || "")).replace(/%DESCRIPTION%/g, () => esc(p.description || "")).replace(/%PATH%/g, () => p.path).replace("%JSONLD%", () => jsonLd).replace("%HEAD%", () => p.head || "").replace(slot, () => p.html);
|
|
465
|
+
const left = /%(TITLE|DESCRIPTION|PATH|JSONLD|HEAD)%/.exec(out);
|
|
466
|
+
if (left) throw new Error(`landing-kit prerenderPages: unfilled ${left[0]} on ${p.path}`);
|
|
467
|
+
outputs.push([p.path, pageFile(p.path, { homeFile, pagesDir }), out]);
|
|
468
|
+
}
|
|
469
|
+
fs.rmSync(path.join(dist, pagesDir), { recursive: true, force: true });
|
|
470
|
+
const map = {};
|
|
471
|
+
let bytes = 0;
|
|
472
|
+
for (const [route, file, out] of outputs) {
|
|
473
|
+
fs.mkdirSync(path.dirname(path.join(dist, file)), { recursive: true });
|
|
474
|
+
fs.writeFileSync(path.join(dist, file), out);
|
|
475
|
+
map[route] = file;
|
|
476
|
+
bytes += out.length;
|
|
477
|
+
}
|
|
478
|
+
const mdir = path.posix.dirname(manifest.split(path.sep).join("/"));
|
|
479
|
+
const rel = mdir === "." ? map : Object.fromEntries(Object.entries(map).map(([r, f]) => [r, path.posix.relative(mdir, f)]));
|
|
480
|
+
fs.mkdirSync(path.dirname(path.join(dist, manifest)), { recursive: true });
|
|
481
|
+
fs.writeFileSync(path.join(dist, manifest), JSON.stringify(rel, null, 2));
|
|
482
|
+
for (const [name, body] of Object.entries(files)) {
|
|
483
|
+
if (body != null) fs.writeFileSync(path.join(dist, name), body);
|
|
484
|
+
}
|
|
485
|
+
return { manifest: rel, bytes };
|
|
486
|
+
}
|
|
487
|
+
export {
|
|
488
|
+
AI_CRAWLERS,
|
|
489
|
+
CRAWLER_FILES,
|
|
490
|
+
DEFAULT_LOCALE,
|
|
491
|
+
DEFAULT_PRIVATE_PATHS,
|
|
492
|
+
PRIVATE_ROBOTS,
|
|
493
|
+
SITE_PLACEHOLDER,
|
|
494
|
+
buildHead,
|
|
495
|
+
buildJsonLd,
|
|
496
|
+
buildLlmsFullTxt,
|
|
497
|
+
buildLlmsTxt,
|
|
498
|
+
buildRobots,
|
|
499
|
+
buildSitemap,
|
|
500
|
+
contentFacts,
|
|
501
|
+
escapeHtml,
|
|
502
|
+
fillSite,
|
|
503
|
+
jsonLdScript,
|
|
504
|
+
landingRoutes,
|
|
505
|
+
pageFile,
|
|
506
|
+
pageUrl,
|
|
507
|
+
prerenderPages,
|
|
508
|
+
readCookie,
|
|
509
|
+
sessionCheck,
|
|
510
|
+
siteUrl,
|
|
511
|
+
verificationTags
|
|
512
|
+
};
|
package/enhance.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EnhanceOptions {
|
|
2
|
+
/** Open the demo form for the clicked button (or null for a #book-demo link). */
|
|
3
|
+
openDemo?: (trigger: HTMLElement | null | undefined) => void | Promise<void>;
|
|
4
|
+
/** Lazily import a module exporting `openDemo` (e.g. () => import('./demoIsland.jsx')). Warmed on hover/focus. */
|
|
5
|
+
loadDemo?: () => Promise<{ openDemo: (trigger?: HTMLElement | null) => void }> | { openDemo: (trigger?: HTMLElement | null) => void };
|
|
6
|
+
warmDemo?: () => unknown;
|
|
7
|
+
/** URL hash that opens the form on load. Default '#book-demo'; '' disables. */
|
|
8
|
+
hash?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Wire a prerendered, un-hydrated landing page. Returns a function that removes the listeners. */
|
|
11
|
+
export declare function enhanceLanding(root?: Document | Element, options?: EnhanceOptions): () => void;
|
|
12
|
+
export default enhanceLanding;
|