@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/dist/seo.cjs ADDED
@@ -0,0 +1,343 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/seo.js
20
+ var seo_exports = {};
21
+ __export(seo_exports, {
22
+ AI_CRAWLERS: () => AI_CRAWLERS,
23
+ DEFAULT_LOCALE: () => DEFAULT_LOCALE,
24
+ DEFAULT_PRIVATE_PATHS: () => DEFAULT_PRIVATE_PATHS,
25
+ PRIVATE_ROBOTS: () => PRIVATE_ROBOTS,
26
+ SITE_PLACEHOLDER: () => SITE_PLACEHOLDER,
27
+ buildHead: () => buildHead,
28
+ buildJsonLd: () => buildJsonLd,
29
+ buildLlmsFullTxt: () => buildLlmsFullTxt,
30
+ buildLlmsTxt: () => buildLlmsTxt,
31
+ buildRobots: () => buildRobots,
32
+ buildSitemap: () => buildSitemap,
33
+ contentFacts: () => contentFacts,
34
+ escapeHtml: () => escapeHtml,
35
+ jsonLdScript: () => jsonLdScript,
36
+ pageUrl: () => pageUrl
37
+ });
38
+ module.exports = __toCommonJS(seo_exports);
39
+ var SITE_PLACEHOLDER = "__SITE_URL__";
40
+ var AI_CRAWLERS = [
41
+ "GPTBot",
42
+ "OAI-SearchBot",
43
+ "ChatGPT-User",
44
+ "ClaudeBot",
45
+ "Claude-SearchBot",
46
+ "Claude-User",
47
+ "PerplexityBot",
48
+ "Perplexity-User",
49
+ "Google-Extended",
50
+ "Applebot-Extended",
51
+ "Bingbot",
52
+ "CCBot"
53
+ ];
54
+ var DEFAULT_PRIVATE_PATHS = ["/api/", "/oauth/", "/.well-known/", "/mcp"];
55
+ var PRIVATE_ROBOTS = "User-agent: *\nDisallow: /\n";
56
+ var DEFAULT_LOCALE = { lang: "en-IN", og: "en_IN", country: "India" };
57
+ var DEFAULT_IMAGE = { path: "/og-image.png", width: 1200, height: 630, type: "image/png" };
58
+ function escapeHtml(value) {
59
+ return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
60
+ }
61
+ function jsonLdScript(graph) {
62
+ return JSON.stringify(graph).replace(/</g, "\\u003c");
63
+ }
64
+ function pageUrl(path = "/", site = SITE_PLACEHOLDER) {
65
+ if (/^https?:\/\//.test(path)) return path;
66
+ if (site == null) return void 0;
67
+ const base = String(site).replace(/\/+$/, "");
68
+ return path === "/" ? `${base}/` : `${base}${path.startsWith("/") ? path : `/${path}`}`;
69
+ }
70
+ var imageOf = (product) => {
71
+ const img = product.image;
72
+ if (!img) return DEFAULT_IMAGE;
73
+ if (typeof img === "string") return { ...DEFAULT_IMAGE, path: img, type: /\.png$/i.test(img) ? "image/png" : void 0 };
74
+ return { ...DEFAULT_IMAGE, ...img };
75
+ };
76
+ var need = (fn, obj, keys) => {
77
+ for (const k of keys) {
78
+ const v = k.split(".").reduce((o, p) => o == null ? o : o[p], obj);
79
+ if (v == null || v === "") throw new Error(`${fn}: "${k}" is missing \u2014 pass it in the ${k.split(".")[0]} object.`);
80
+ }
81
+ };
82
+ function buildJsonLd({
83
+ site = SITE_PLACEHOLDER,
84
+ product,
85
+ org = { name: "Dreamtree-Org" },
86
+ page = { path: "/" },
87
+ faq,
88
+ howTo,
89
+ breadcrumbs,
90
+ parts,
91
+ keywords,
92
+ lastmod,
93
+ speakable,
94
+ locale = DEFAULT_LOCALE
95
+ }) {
96
+ need("buildJsonLd", { product, page }, ["product.name", "product.definition"]);
97
+ const u = (p) => pageUrl(p, site);
98
+ const home = u("/");
99
+ const id = (frag, base = home) => base ? `${base}#${frag}` : void 0;
100
+ const ref = (frag) => home ? { "@id": id(frag) } : void 0;
101
+ const url = u(page.path || "/");
102
+ const image = imageOf(product);
103
+ const imageUrl = u(image.path);
104
+ const sameAs = (org.sameAs || []).filter((s) => /^https?:\/\//.test(s));
105
+ const graph = [
106
+ {
107
+ "@type": "Organization",
108
+ "@id": id("organization"),
109
+ name: org.name,
110
+ url: home,
111
+ ...org.logo ? { logo: { "@type": "ImageObject", url: u(org.logo) } } : {},
112
+ ...product.logo ? { brand: { "@type": "Brand", name: product.name, logo: u(product.logo) } } : { brand: { "@type": "Brand", name: product.name } },
113
+ ...org.email ? { email: org.email, contactPoint: { "@type": "ContactPoint", contactType: "sales", email: org.email } } : {},
114
+ ...sameAs.length ? { sameAs } : {}
115
+ },
116
+ {
117
+ "@type": "WebSite",
118
+ "@id": id("website"),
119
+ url: home,
120
+ name: product.name,
121
+ ...product.description ? { description: product.description } : {},
122
+ publisher: ref("organization"),
123
+ inLanguage: locale.lang
124
+ },
125
+ {
126
+ "@type": "SoftwareApplication",
127
+ "@id": id("software"),
128
+ name: product.name,
129
+ description: product.definition,
130
+ applicationCategory: product.category || "BusinessApplication",
131
+ ...product.subCategory ? { applicationSubCategory: product.subCategory } : {},
132
+ operatingSystem: product.operatingSystem || "Web browser",
133
+ url: home,
134
+ image: imageUrl,
135
+ ...product.screenshot ? { screenshot: u(product.screenshot) } : {},
136
+ ...product.features && product.features.length ? { featureList: product.features } : {},
137
+ ...product.audience ? { audience: { "@type": "BusinessAudience", audienceType: product.audience } } : {},
138
+ ...locale.country ? { areaServed: { "@type": "Country", name: locale.country } } : {},
139
+ publisher: ref("organization"),
140
+ inLanguage: locale.lang,
141
+ ...product.offers ? {
142
+ offers: {
143
+ "@type": "Offer",
144
+ description: product.offers.description || "Contact us for pricing",
145
+ url: u(product.offers.url || "/#book-demo"),
146
+ availability: "https://schema.org/InStock",
147
+ seller: ref("organization")
148
+ }
149
+ } : {}
150
+ }
151
+ ];
152
+ if (page.title) {
153
+ graph.push({
154
+ "@type": "WebPage",
155
+ "@id": id("webpage", url),
156
+ url,
157
+ name: page.title,
158
+ ...page.description ? { description: page.description } : {},
159
+ isPartOf: ref("website"),
160
+ about: ref("software"),
161
+ primaryImageOfPage: { "@type": "ImageObject", url: imageUrl, width: image.width, height: image.height },
162
+ inLanguage: locale.lang,
163
+ ...lastmod ? { dateModified: lastmod } : {},
164
+ ...speakable && speakable.length ? { speakable: { "@type": "SpeakableSpecification", cssSelector: speakable } } : {},
165
+ ...url ? { breadcrumb: { "@id": id("breadcrumb", url) } } : {},
166
+ ...keywords && keywords.length ? { keywords: keywords.join(", ") } : {},
167
+ ...parts && parts.length ? { hasPart: parts.map((p) => ({ "@type": "WebPage", url: u(p.path), name: p.name })) } : {}
168
+ });
169
+ const trail = breadcrumbs || (page.path && page.path !== "/" ? [[product.name, "/"], [page.name || page.title, page.path]] : [[product.name, "/"]]);
170
+ graph.push({
171
+ "@type": "BreadcrumbList",
172
+ "@id": id("breadcrumb", url),
173
+ itemListElement: trail.map(([name, p], i) => ({ "@type": "ListItem", position: i + 1, name, item: u(p) }))
174
+ });
175
+ }
176
+ if (faq && faq.length) {
177
+ graph.push({
178
+ "@type": "FAQPage",
179
+ "@id": id("faq", url),
180
+ ...page.title && url ? { isPartOf: { "@id": id("webpage", url) } } : {},
181
+ mainEntity: faq.map((f) => ({ "@type": "Question", name: f.q, acceptedAnswer: { "@type": "Answer", text: f.a } }))
182
+ });
183
+ }
184
+ if (howTo && howTo.steps && howTo.steps.length) {
185
+ const anchor = howTo.anchor || "how-it-works";
186
+ graph.push({
187
+ "@type": "HowTo",
188
+ "@id": id(anchor, url),
189
+ name: howTo.name || `How to use ${product.name}`,
190
+ ...howTo.description ? { description: howTo.description } : {},
191
+ tool: { "@type": "HowToTool", name: product.name },
192
+ step: howTo.steps.map((st, i) => ({
193
+ "@type": "HowToStep",
194
+ position: i + 1,
195
+ name: st.name || st.title,
196
+ text: st.text || st.body,
197
+ ...url ? { url: `${url}#${anchor}` } : {}
198
+ }))
199
+ });
200
+ }
201
+ return { "@context": "https://schema.org", "@graph": JSON.parse(JSON.stringify(graph)) };
202
+ }
203
+ function contentFacts(content = {}) {
204
+ const steps = content.steps ? content.steps.steps || content.steps.items : null;
205
+ const sections = [];
206
+ for (const s of content.sections || []) {
207
+ if (!s || !s.title || s.use) continue;
208
+ const items = (s.items || []).map((i) => typeof i === "string" ? `- ${i}` : `- ${i.title}${i.body ? `: ${i.body}` : ""}`);
209
+ const extra = [s.detail, s.note].filter(Boolean).join(" ");
210
+ sections.push([s.eyebrow || s.title, [s.body, extra, items.join("\n")].filter(Boolean).join("\n\n")]);
211
+ }
212
+ if (content.audiences) {
213
+ sections.push([content.audiences.title, (content.audiences.items || []).map((a) => `- ${a.title}: ${a.body}`).join("\n")]);
214
+ }
215
+ if (steps) sections.push([content.steps.title, steps.map((st, i) => `${i + 1}. ${st.name || st.title}: ${st.text || st.body}`).join("\n")]);
216
+ return {
217
+ faq: content.faq ? content.faq.items || [] : [],
218
+ howTo: steps ? { name: content.steps.howToName, description: content.steps.title, steps, anchor: content.steps.id || "how-it-works" } : null,
219
+ features: content.product && content.product.features || [],
220
+ sections
221
+ };
222
+ }
223
+ function buildHead({ site = SITE_PLACEHOLDER, page, product, twitterHandle, jsonLd, locale = DEFAULT_LOCALE, robots }) {
224
+ need("buildHead", { page, product }, ["page.title", "page.description", "product.name"]);
225
+ const e = escapeHtml;
226
+ const url = pageUrl(page.path || "/", site);
227
+ const image = imageOf(product);
228
+ const imageUrl = pageUrl(image.path, site);
229
+ const alt = image.alt || `${product.name}: ${page.name && page.name !== "Home" ? page.name : page.title}`;
230
+ const handle = twitterHandle ? `@${String(twitterHandle).replace(/^@/, "")}` : "";
231
+ return [
232
+ `<title>${e(page.title)}</title>`,
233
+ `<meta name="description" content="${e(page.description)}" />`,
234
+ `<meta name="robots" content="${e(robots || "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1")}" />`,
235
+ url ? `<link rel="canonical" href="${e(url)}" />` : null,
236
+ url ? `<link rel="alternate" hreflang="${locale.lang}" href="${e(url)}" />` : null,
237
+ url ? `<link rel="alternate" hreflang="x-default" href="${e(url)}" />` : null,
238
+ '<meta property="og:type" content="website" />',
239
+ `<meta property="og:site_name" content="${e(product.name)}" />`,
240
+ `<meta property="og:locale" content="${locale.og}" />`,
241
+ url ? `<meta property="og:url" content="${e(url)}" />` : null,
242
+ `<meta property="og:title" content="${e(page.title)}" />`,
243
+ `<meta property="og:description" content="${e(page.description)}" />`,
244
+ imageUrl ? `<meta property="og:image" content="${e(imageUrl)}" />` : null,
245
+ image.type ? `<meta property="og:image:type" content="${e(image.type)}" />` : null,
246
+ `<meta property="og:image:width" content="${image.width}" />`,
247
+ `<meta property="og:image:height" content="${image.height}" />`,
248
+ `<meta property="og:image:alt" content="${e(alt)}" />`,
249
+ '<meta name="twitter:card" content="summary_large_image" />',
250
+ handle ? `<meta name="twitter:site" content="${e(handle)}" />` : null,
251
+ `<meta name="twitter:title" content="${e(page.title)}" />`,
252
+ `<meta name="twitter:description" content="${e(page.description)}" />`,
253
+ imageUrl ? `<meta name="twitter:image" content="${e(imageUrl)}" />` : null,
254
+ site != null ? `<link rel="alternate" type="text/plain" title="LLM summary" href="${e(pageUrl("/llms.txt", site))}" />` : null,
255
+ jsonLd ? `<script type="application/ld+json">${jsonLdScript(jsonLd)}</script>` : null
256
+ ].filter(Boolean).join("\n ");
257
+ }
258
+ function buildRobots({ site = SITE_PLACEHOLDER, privatePaths = DEFAULT_PRIVATE_PATHS, crawlers = AI_CRAWLERS } = {}) {
259
+ const rules = ["Allow: /", ...privatePaths.map((p) => `Disallow: ${p}`)];
260
+ const blocks = ["*", ...crawlers].map((agent) => [`User-agent: ${agent}`, ...rules].join("\n"));
261
+ const map = pageUrl("/sitemap.xml", site);
262
+ return `${blocks.join("\n\n")}
263
+ ${map ? `
264
+ Sitemap: ${map}
265
+ ` : ""}`;
266
+ }
267
+ function buildSitemap({ site = SITE_PLACEHOLDER, pages, lastmod, changefreq = "monthly" }) {
268
+ if (!Array.isArray(pages) || !pages.length) throw new Error('buildSitemap: "pages" is missing \u2014 pass [{ path: "/" }, ...].');
269
+ return [
270
+ '<?xml version="1.0" encoding="UTF-8"?>',
271
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
272
+ ...pages.map((p) => {
273
+ const pr = p.priority || (p.path === "/" ? "1.0" : "0.8");
274
+ return ` <url><loc>${escapeHtml(pageUrl(p.path, site))}</loc>${lastmod ? `<lastmod>${lastmod}</lastmod>` : ""}<changefreq>${p.changefreq || changefreq}</changefreq><priority>${pr}</priority></url>`;
275
+ }),
276
+ "</urlset>",
277
+ ""
278
+ ].join("\n");
279
+ }
280
+ function buildLlmsTxt({ site = SITE_PLACEHOLDER, product, org = { name: "Dreamtree-Org" }, pages = [], about, facts, optional }) {
281
+ need("buildLlmsTxt", { product }, ["product.name", "product.definition"]);
282
+ const u = (p) => pageUrl(p, site);
283
+ return [
284
+ `# ${product.name}`,
285
+ "",
286
+ `> ${product.definition}`,
287
+ "",
288
+ about || `${product.name} is made by ${org.name}. It runs in the web browser.`,
289
+ "",
290
+ "## Pages",
291
+ "",
292
+ ...pages.map((p) => `- [${p.path === "/" ? `${product.name} home` : p.name || p.title}](${u(p.path)}): ${p.summary || p.description}`),
293
+ `- [Full product description for language models](${u("/llms-full.txt")}): every feature, page and FAQ in plain text`,
294
+ "",
295
+ "## What it does",
296
+ "",
297
+ ...(facts || product.features || []).map((f) => `- ${f}`),
298
+ "",
299
+ "## Optional",
300
+ "",
301
+ ...(optional || [
302
+ { label: "Book a demo", path: "/#book-demo" },
303
+ { label: "Log in", path: "/login" }
304
+ ]).map((o) => `- [${o.label}](${u(o.path)})`),
305
+ ""
306
+ ].join("\n");
307
+ }
308
+ function buildLlmsFullTxt({ site = SITE_PLACEHOLDER, product, org = { name: "Dreamtree-Org" }, pages = [], sections = () => [] }) {
309
+ need("buildLlmsFullTxt", { product }, ["product.name", "product.definition"]);
310
+ const out = [`# ${product.name}: full description`, "", `> ${product.definition}`, "", `Made by ${org.name}. Source: ${pageUrl("/", site)}`, ""];
311
+ for (const page of pages) {
312
+ out.push(`## ${page.path === "/" ? "Overview" : page.name || page.title}`, "", `Page: ${pageUrl(page.path, site)}`, "");
313
+ if (page.description) out.push(page.description, "");
314
+ for (const [heading, body] of sections(page) || []) {
315
+ out.push(`### ${heading}`, "");
316
+ if (body) out.push(body, "");
317
+ }
318
+ if (page.faq && page.faq.length) {
319
+ out.push("### Frequently asked questions", "");
320
+ for (const f of page.faq) out.push(`**${f.q}**`, "", f.a, "");
321
+ }
322
+ }
323
+ if (product.features && product.features.length) out.push("## Features", "", ...product.features.map((f) => `- ${f}`), "");
324
+ return out.join("\n");
325
+ }
326
+ // Annotate the CommonJS export names for ESM import in node:
327
+ 0 && (module.exports = {
328
+ AI_CRAWLERS,
329
+ DEFAULT_LOCALE,
330
+ DEFAULT_PRIVATE_PATHS,
331
+ PRIVATE_ROBOTS,
332
+ SITE_PLACEHOLDER,
333
+ buildHead,
334
+ buildJsonLd,
335
+ buildLlmsFullTxt,
336
+ buildLlmsTxt,
337
+ buildRobots,
338
+ buildSitemap,
339
+ contentFacts,
340
+ escapeHtml,
341
+ jsonLdScript,
342
+ pageUrl
343
+ });
package/dist/seo.js ADDED
@@ -0,0 +1,305 @@
1
+ // src/seo.js
2
+ var SITE_PLACEHOLDER = "__SITE_URL__";
3
+ var AI_CRAWLERS = [
4
+ "GPTBot",
5
+ "OAI-SearchBot",
6
+ "ChatGPT-User",
7
+ "ClaudeBot",
8
+ "Claude-SearchBot",
9
+ "Claude-User",
10
+ "PerplexityBot",
11
+ "Perplexity-User",
12
+ "Google-Extended",
13
+ "Applebot-Extended",
14
+ "Bingbot",
15
+ "CCBot"
16
+ ];
17
+ var DEFAULT_PRIVATE_PATHS = ["/api/", "/oauth/", "/.well-known/", "/mcp"];
18
+ var PRIVATE_ROBOTS = "User-agent: *\nDisallow: /\n";
19
+ var DEFAULT_LOCALE = { lang: "en-IN", og: "en_IN", country: "India" };
20
+ var DEFAULT_IMAGE = { path: "/og-image.png", width: 1200, height: 630, type: "image/png" };
21
+ function escapeHtml(value) {
22
+ return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
23
+ }
24
+ function jsonLdScript(graph) {
25
+ return JSON.stringify(graph).replace(/</g, "\\u003c");
26
+ }
27
+ function pageUrl(path = "/", site = SITE_PLACEHOLDER) {
28
+ if (/^https?:\/\//.test(path)) return path;
29
+ if (site == null) return void 0;
30
+ const base = String(site).replace(/\/+$/, "");
31
+ return path === "/" ? `${base}/` : `${base}${path.startsWith("/") ? path : `/${path}`}`;
32
+ }
33
+ var imageOf = (product) => {
34
+ const img = product.image;
35
+ if (!img) return DEFAULT_IMAGE;
36
+ if (typeof img === "string") return { ...DEFAULT_IMAGE, path: img, type: /\.png$/i.test(img) ? "image/png" : void 0 };
37
+ return { ...DEFAULT_IMAGE, ...img };
38
+ };
39
+ var need = (fn, obj, keys) => {
40
+ for (const k of keys) {
41
+ const v = k.split(".").reduce((o, p) => o == null ? o : o[p], obj);
42
+ if (v == null || v === "") throw new Error(`${fn}: "${k}" is missing \u2014 pass it in the ${k.split(".")[0]} object.`);
43
+ }
44
+ };
45
+ function buildJsonLd({
46
+ site = SITE_PLACEHOLDER,
47
+ product,
48
+ org = { name: "Dreamtree-Org" },
49
+ page = { path: "/" },
50
+ faq,
51
+ howTo,
52
+ breadcrumbs,
53
+ parts,
54
+ keywords,
55
+ lastmod,
56
+ speakable,
57
+ locale = DEFAULT_LOCALE
58
+ }) {
59
+ need("buildJsonLd", { product, page }, ["product.name", "product.definition"]);
60
+ const u = (p) => pageUrl(p, site);
61
+ const home = u("/");
62
+ const id = (frag, base = home) => base ? `${base}#${frag}` : void 0;
63
+ const ref = (frag) => home ? { "@id": id(frag) } : void 0;
64
+ const url = u(page.path || "/");
65
+ const image = imageOf(product);
66
+ const imageUrl = u(image.path);
67
+ const sameAs = (org.sameAs || []).filter((s) => /^https?:\/\//.test(s));
68
+ const graph = [
69
+ {
70
+ "@type": "Organization",
71
+ "@id": id("organization"),
72
+ name: org.name,
73
+ url: home,
74
+ ...org.logo ? { logo: { "@type": "ImageObject", url: u(org.logo) } } : {},
75
+ ...product.logo ? { brand: { "@type": "Brand", name: product.name, logo: u(product.logo) } } : { brand: { "@type": "Brand", name: product.name } },
76
+ ...org.email ? { email: org.email, contactPoint: { "@type": "ContactPoint", contactType: "sales", email: org.email } } : {},
77
+ ...sameAs.length ? { sameAs } : {}
78
+ },
79
+ {
80
+ "@type": "WebSite",
81
+ "@id": id("website"),
82
+ url: home,
83
+ name: product.name,
84
+ ...product.description ? { description: product.description } : {},
85
+ publisher: ref("organization"),
86
+ inLanguage: locale.lang
87
+ },
88
+ {
89
+ "@type": "SoftwareApplication",
90
+ "@id": id("software"),
91
+ name: product.name,
92
+ description: product.definition,
93
+ applicationCategory: product.category || "BusinessApplication",
94
+ ...product.subCategory ? { applicationSubCategory: product.subCategory } : {},
95
+ operatingSystem: product.operatingSystem || "Web browser",
96
+ url: home,
97
+ image: imageUrl,
98
+ ...product.screenshot ? { screenshot: u(product.screenshot) } : {},
99
+ ...product.features && product.features.length ? { featureList: product.features } : {},
100
+ ...product.audience ? { audience: { "@type": "BusinessAudience", audienceType: product.audience } } : {},
101
+ ...locale.country ? { areaServed: { "@type": "Country", name: locale.country } } : {},
102
+ publisher: ref("organization"),
103
+ inLanguage: locale.lang,
104
+ ...product.offers ? {
105
+ offers: {
106
+ "@type": "Offer",
107
+ description: product.offers.description || "Contact us for pricing",
108
+ url: u(product.offers.url || "/#book-demo"),
109
+ availability: "https://schema.org/InStock",
110
+ seller: ref("organization")
111
+ }
112
+ } : {}
113
+ }
114
+ ];
115
+ if (page.title) {
116
+ graph.push({
117
+ "@type": "WebPage",
118
+ "@id": id("webpage", url),
119
+ url,
120
+ name: page.title,
121
+ ...page.description ? { description: page.description } : {},
122
+ isPartOf: ref("website"),
123
+ about: ref("software"),
124
+ primaryImageOfPage: { "@type": "ImageObject", url: imageUrl, width: image.width, height: image.height },
125
+ inLanguage: locale.lang,
126
+ ...lastmod ? { dateModified: lastmod } : {},
127
+ ...speakable && speakable.length ? { speakable: { "@type": "SpeakableSpecification", cssSelector: speakable } } : {},
128
+ ...url ? { breadcrumb: { "@id": id("breadcrumb", url) } } : {},
129
+ ...keywords && keywords.length ? { keywords: keywords.join(", ") } : {},
130
+ ...parts && parts.length ? { hasPart: parts.map((p) => ({ "@type": "WebPage", url: u(p.path), name: p.name })) } : {}
131
+ });
132
+ const trail = breadcrumbs || (page.path && page.path !== "/" ? [[product.name, "/"], [page.name || page.title, page.path]] : [[product.name, "/"]]);
133
+ graph.push({
134
+ "@type": "BreadcrumbList",
135
+ "@id": id("breadcrumb", url),
136
+ itemListElement: trail.map(([name, p], i) => ({ "@type": "ListItem", position: i + 1, name, item: u(p) }))
137
+ });
138
+ }
139
+ if (faq && faq.length) {
140
+ graph.push({
141
+ "@type": "FAQPage",
142
+ "@id": id("faq", url),
143
+ ...page.title && url ? { isPartOf: { "@id": id("webpage", url) } } : {},
144
+ mainEntity: faq.map((f) => ({ "@type": "Question", name: f.q, acceptedAnswer: { "@type": "Answer", text: f.a } }))
145
+ });
146
+ }
147
+ if (howTo && howTo.steps && howTo.steps.length) {
148
+ const anchor = howTo.anchor || "how-it-works";
149
+ graph.push({
150
+ "@type": "HowTo",
151
+ "@id": id(anchor, url),
152
+ name: howTo.name || `How to use ${product.name}`,
153
+ ...howTo.description ? { description: howTo.description } : {},
154
+ tool: { "@type": "HowToTool", name: product.name },
155
+ step: howTo.steps.map((st, i) => ({
156
+ "@type": "HowToStep",
157
+ position: i + 1,
158
+ name: st.name || st.title,
159
+ text: st.text || st.body,
160
+ ...url ? { url: `${url}#${anchor}` } : {}
161
+ }))
162
+ });
163
+ }
164
+ return { "@context": "https://schema.org", "@graph": JSON.parse(JSON.stringify(graph)) };
165
+ }
166
+ function contentFacts(content = {}) {
167
+ const steps = content.steps ? content.steps.steps || content.steps.items : null;
168
+ const sections = [];
169
+ for (const s of content.sections || []) {
170
+ if (!s || !s.title || s.use) continue;
171
+ const items = (s.items || []).map((i) => typeof i === "string" ? `- ${i}` : `- ${i.title}${i.body ? `: ${i.body}` : ""}`);
172
+ const extra = [s.detail, s.note].filter(Boolean).join(" ");
173
+ sections.push([s.eyebrow || s.title, [s.body, extra, items.join("\n")].filter(Boolean).join("\n\n")]);
174
+ }
175
+ if (content.audiences) {
176
+ sections.push([content.audiences.title, (content.audiences.items || []).map((a) => `- ${a.title}: ${a.body}`).join("\n")]);
177
+ }
178
+ if (steps) sections.push([content.steps.title, steps.map((st, i) => `${i + 1}. ${st.name || st.title}: ${st.text || st.body}`).join("\n")]);
179
+ return {
180
+ faq: content.faq ? content.faq.items || [] : [],
181
+ howTo: steps ? { name: content.steps.howToName, description: content.steps.title, steps, anchor: content.steps.id || "how-it-works" } : null,
182
+ features: content.product && content.product.features || [],
183
+ sections
184
+ };
185
+ }
186
+ function buildHead({ site = SITE_PLACEHOLDER, page, product, twitterHandle, jsonLd, locale = DEFAULT_LOCALE, robots }) {
187
+ need("buildHead", { page, product }, ["page.title", "page.description", "product.name"]);
188
+ const e = escapeHtml;
189
+ const url = pageUrl(page.path || "/", site);
190
+ const image = imageOf(product);
191
+ const imageUrl = pageUrl(image.path, site);
192
+ const alt = image.alt || `${product.name}: ${page.name && page.name !== "Home" ? page.name : page.title}`;
193
+ const handle = twitterHandle ? `@${String(twitterHandle).replace(/^@/, "")}` : "";
194
+ return [
195
+ `<title>${e(page.title)}</title>`,
196
+ `<meta name="description" content="${e(page.description)}" />`,
197
+ `<meta name="robots" content="${e(robots || "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1")}" />`,
198
+ url ? `<link rel="canonical" href="${e(url)}" />` : null,
199
+ url ? `<link rel="alternate" hreflang="${locale.lang}" href="${e(url)}" />` : null,
200
+ url ? `<link rel="alternate" hreflang="x-default" href="${e(url)}" />` : null,
201
+ '<meta property="og:type" content="website" />',
202
+ `<meta property="og:site_name" content="${e(product.name)}" />`,
203
+ `<meta property="og:locale" content="${locale.og}" />`,
204
+ url ? `<meta property="og:url" content="${e(url)}" />` : null,
205
+ `<meta property="og:title" content="${e(page.title)}" />`,
206
+ `<meta property="og:description" content="${e(page.description)}" />`,
207
+ imageUrl ? `<meta property="og:image" content="${e(imageUrl)}" />` : null,
208
+ image.type ? `<meta property="og:image:type" content="${e(image.type)}" />` : null,
209
+ `<meta property="og:image:width" content="${image.width}" />`,
210
+ `<meta property="og:image:height" content="${image.height}" />`,
211
+ `<meta property="og:image:alt" content="${e(alt)}" />`,
212
+ '<meta name="twitter:card" content="summary_large_image" />',
213
+ handle ? `<meta name="twitter:site" content="${e(handle)}" />` : null,
214
+ `<meta name="twitter:title" content="${e(page.title)}" />`,
215
+ `<meta name="twitter:description" content="${e(page.description)}" />`,
216
+ imageUrl ? `<meta name="twitter:image" content="${e(imageUrl)}" />` : null,
217
+ site != null ? `<link rel="alternate" type="text/plain" title="LLM summary" href="${e(pageUrl("/llms.txt", site))}" />` : null,
218
+ jsonLd ? `<script type="application/ld+json">${jsonLdScript(jsonLd)}</script>` : null
219
+ ].filter(Boolean).join("\n ");
220
+ }
221
+ function buildRobots({ site = SITE_PLACEHOLDER, privatePaths = DEFAULT_PRIVATE_PATHS, crawlers = AI_CRAWLERS } = {}) {
222
+ const rules = ["Allow: /", ...privatePaths.map((p) => `Disallow: ${p}`)];
223
+ const blocks = ["*", ...crawlers].map((agent) => [`User-agent: ${agent}`, ...rules].join("\n"));
224
+ const map = pageUrl("/sitemap.xml", site);
225
+ return `${blocks.join("\n\n")}
226
+ ${map ? `
227
+ Sitemap: ${map}
228
+ ` : ""}`;
229
+ }
230
+ function buildSitemap({ site = SITE_PLACEHOLDER, pages, lastmod, changefreq = "monthly" }) {
231
+ if (!Array.isArray(pages) || !pages.length) throw new Error('buildSitemap: "pages" is missing \u2014 pass [{ path: "/" }, ...].');
232
+ return [
233
+ '<?xml version="1.0" encoding="UTF-8"?>',
234
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
235
+ ...pages.map((p) => {
236
+ const pr = p.priority || (p.path === "/" ? "1.0" : "0.8");
237
+ return ` <url><loc>${escapeHtml(pageUrl(p.path, site))}</loc>${lastmod ? `<lastmod>${lastmod}</lastmod>` : ""}<changefreq>${p.changefreq || changefreq}</changefreq><priority>${pr}</priority></url>`;
238
+ }),
239
+ "</urlset>",
240
+ ""
241
+ ].join("\n");
242
+ }
243
+ function buildLlmsTxt({ site = SITE_PLACEHOLDER, product, org = { name: "Dreamtree-Org" }, pages = [], about, facts, optional }) {
244
+ need("buildLlmsTxt", { product }, ["product.name", "product.definition"]);
245
+ const u = (p) => pageUrl(p, site);
246
+ return [
247
+ `# ${product.name}`,
248
+ "",
249
+ `> ${product.definition}`,
250
+ "",
251
+ about || `${product.name} is made by ${org.name}. It runs in the web browser.`,
252
+ "",
253
+ "## Pages",
254
+ "",
255
+ ...pages.map((p) => `- [${p.path === "/" ? `${product.name} home` : p.name || p.title}](${u(p.path)}): ${p.summary || p.description}`),
256
+ `- [Full product description for language models](${u("/llms-full.txt")}): every feature, page and FAQ in plain text`,
257
+ "",
258
+ "## What it does",
259
+ "",
260
+ ...(facts || product.features || []).map((f) => `- ${f}`),
261
+ "",
262
+ "## Optional",
263
+ "",
264
+ ...(optional || [
265
+ { label: "Book a demo", path: "/#book-demo" },
266
+ { label: "Log in", path: "/login" }
267
+ ]).map((o) => `- [${o.label}](${u(o.path)})`),
268
+ ""
269
+ ].join("\n");
270
+ }
271
+ function buildLlmsFullTxt({ site = SITE_PLACEHOLDER, product, org = { name: "Dreamtree-Org" }, pages = [], sections = () => [] }) {
272
+ need("buildLlmsFullTxt", { product }, ["product.name", "product.definition"]);
273
+ const out = [`# ${product.name}: full description`, "", `> ${product.definition}`, "", `Made by ${org.name}. Source: ${pageUrl("/", site)}`, ""];
274
+ for (const page of pages) {
275
+ out.push(`## ${page.path === "/" ? "Overview" : page.name || page.title}`, "", `Page: ${pageUrl(page.path, site)}`, "");
276
+ if (page.description) out.push(page.description, "");
277
+ for (const [heading, body] of sections(page) || []) {
278
+ out.push(`### ${heading}`, "");
279
+ if (body) out.push(body, "");
280
+ }
281
+ if (page.faq && page.faq.length) {
282
+ out.push("### Frequently asked questions", "");
283
+ for (const f of page.faq) out.push(`**${f.q}**`, "", f.a, "");
284
+ }
285
+ }
286
+ if (product.features && product.features.length) out.push("## Features", "", ...product.features.map((f) => `- ${f}`), "");
287
+ return out.join("\n");
288
+ }
289
+ export {
290
+ AI_CRAWLERS,
291
+ DEFAULT_LOCALE,
292
+ DEFAULT_PRIVATE_PATHS,
293
+ PRIVATE_ROBOTS,
294
+ SITE_PLACEHOLDER,
295
+ buildHead,
296
+ buildJsonLd,
297
+ buildLlmsFullTxt,
298
+ buildLlmsTxt,
299
+ buildRobots,
300
+ buildSitemap,
301
+ contentFacts,
302
+ escapeHtml,
303
+ jsonLdScript,
304
+ pageUrl
305
+ };