@rimelight/seo 0.0.4 → 0.0.6

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.
Files changed (46) hide show
  1. package/dist/index.d.mts +8 -38
  2. package/dist/index.mjs +8 -175
  3. package/dist/integration.d.mts +6 -0
  4. package/dist/integration.mjs +101 -0
  5. package/dist/llms-BVLxzjr-.d.mts +41 -0
  6. package/dist/llms.d.mts +2 -0
  7. package/dist/llms.mjs +75 -0
  8. package/dist/meta.d.mts +42 -0
  9. package/dist/meta.mjs +66 -0
  10. package/dist/og.d.mts +117 -0
  11. package/dist/og.mjs +302 -0
  12. package/dist/robots.d.mts +3 -4
  13. package/dist/routes/llms-full.txt.d.mts +5 -0
  14. package/dist/routes/llms-full.txt.mjs +57 -0
  15. package/dist/routes/llms.txt.d.mts +5 -0
  16. package/dist/routes/llms.txt.mjs +68 -0
  17. package/dist/routes/robots.txt.d.mts +4 -0
  18. package/dist/routes/robots.txt.mjs +46 -0
  19. package/dist/routes/rss.xml.d.mts +4 -0
  20. package/dist/routes/rss.xml.mjs +51 -0
  21. package/dist/routes/sitemap.xml.d.mts +4 -0
  22. package/dist/routes/sitemap.xml.mjs +27 -0
  23. package/dist/rss.d.mts +8 -0
  24. package/dist/rss.mjs +44 -0
  25. package/dist/schema-C8v69blQ.d.mts +28 -0
  26. package/dist/schema.d.mts +2 -0
  27. package/dist/schema.mjs +52 -0
  28. package/dist/sitemap.d.mts +6 -7
  29. package/dist/types.d.mts +354 -2
  30. package/package.json +24 -5
  31. package/src/components/SEOHead.astro +65 -18
  32. package/src/env.d.ts +33 -3
  33. package/src/index.ts +19 -1
  34. package/src/integration.ts +172 -0
  35. package/src/meta.test.ts +25 -1
  36. package/src/meta.ts +32 -2
  37. package/src/og.ts +444 -0
  38. package/src/routes/llms-full.txt.ts +77 -0
  39. package/src/routes/llms.txt.ts +90 -0
  40. package/src/routes/robots.txt.ts +63 -0
  41. package/src/routes/rss.xml.ts +87 -0
  42. package/src/routes/sitemap.xml.ts +43 -0
  43. package/src/rss.test.ts +28 -0
  44. package/src/rss.ts +63 -0
  45. package/src/types.ts +367 -242
  46. package/dist/types-e7gqYZwc.d.mts +0 -305
package/dist/og.d.mts ADDED
@@ -0,0 +1,117 @@
1
+ //#region src/og.d.ts
2
+ /**
3
+ * Font configuration for OG image rendering. Pass to `loadOgFonts()` or `renderOgResponse()`.
4
+ */
5
+ export interface OgFontConfig {
6
+ /**
7
+ * Font-family name referenced in layout styles
8
+ */
9
+ name: string;
10
+ /**
11
+ * URL to the regular-weight TTF
12
+ */
13
+ regularUrl: string;
14
+ /**
15
+ * URL to the bold-weight TTF
16
+ */
17
+ boldUrl: string;
18
+ }
19
+ /**
20
+ * Default font: Noto Sans from jsDelivr Fontsource CDN.
21
+ */
22
+ export declare const NOTO_SANS: OgFontConfig;
23
+ /**
24
+ * Options for the default OG image layout. Build your own layout instead via `renderOgResponse()`
25
+ * directly.
26
+ */
27
+ export interface OgLayoutOptions {
28
+ /**
29
+ * Main heading -- required
30
+ */
31
+ title: string;
32
+ /**
33
+ * Subtitle / excerpt beneath the title
34
+ */
35
+ description?: string;
36
+ /**
37
+ * Branding shown top-left. Pass a plain string (site name) or a pre-built takumi-js VNode for a
38
+ * custom logo treatment.
39
+ */
40
+ brand?: string | object;
41
+ /**
42
+ * Pill badge shown bottom-left. e.g. "Blog Post", "Documentation", "Legal"
43
+ */
44
+ badge?: string;
45
+ /**
46
+ * Date string shown bottom-right. e.g. new Date(postedAt).toLocaleDateString()
47
+ */
48
+ date?: string;
49
+ /**
50
+ * Background style. "dark" -> solid #0a0a0a (default) "gradient" -> diagonal dark-to-navy
51
+ * gradient any string -> treated as a CSS `background` value
52
+ */
53
+ background?: "dark" | "gradient" | (string & {});
54
+ /**
55
+ * When true, overlays a dashed red PREVIEW border. Useful during development.
56
+ */
57
+ preview?: boolean;
58
+ }
59
+ /**
60
+ * Rendering config shared by `renderOgResponse()` and `renderDefaultOg()`.
61
+ */
62
+ export interface OgRenderConfig {
63
+ /**
64
+ * Font to embed -- defaults to `NOTO_SANS`
65
+ */
66
+ font?: OgFontConfig;
67
+ /**
68
+ * Image width in px -- defaults to 1200
69
+ */
70
+ width?: number;
71
+ /**
72
+ * Image height in px -- defaults to 630
73
+ */
74
+ height?: number;
75
+ }
76
+ /**
77
+ * Fetches and caches the regular + bold TTF buffers for the given font config. Uses a module-level
78
+ * cache keyed on `regularUrl`, so repeated calls within the same Worker lifetime are free after the
79
+ * first fetch.
80
+ */
81
+ export declare function loadOgFonts(config?: OgFontConfig, timeoutMs?: number): Promise<{
82
+ regular: ArrayBuffer;
83
+ bold: ArrayBuffer;
84
+ }>;
85
+ /**
86
+ * Builds a takumi-js-compatible VNode for the default Rimelight OG layout.
87
+ *
88
+ * Consumers that need a fully custom look should build their own VNode and call
89
+ * `renderOgResponse()` directly -- this function is just the house style.
90
+ */
91
+ export declare function defaultOgLayout(options: OgLayoutOptions, font?: OgFontConfig): object;
92
+ /**
93
+ * Renders any takumi-js VNode to a PNG `Response` with standard OG headers. Use this when you
94
+ * supply your own layout entirely.
95
+ *
96
+ * @example
97
+ * const vnode = myBrandedLayout({ title, description })
98
+ * return renderOgResponse(vnode, { font: MY_FONT })
99
+ */
100
+ export declare function renderOgResponse(vnode: object, config?: OgRenderConfig): Promise<Response>;
101
+ /**
102
+ * Renders the default OG layout to a PNG `Response`. The 90% case -- use `renderOgResponse()` for
103
+ * full layout control.
104
+ *
105
+ * @example
106
+ * export const GET: APIRoute = async ({ request }) => {
107
+ * const url = new URL(request.url)
108
+ * return renderDefaultOg({
109
+ * title: url.searchParams.get("title") ?? "Untitled",
110
+ * description: url.searchParams.get("description") ?? "",
111
+ * brand: "My Site",
112
+ * badge: url.searchParams.get("type") ?? ""
113
+ * })
114
+ * }
115
+ */
116
+ export declare function renderDefaultOg(options: OgLayoutOptions, config?: OgRenderConfig): Promise<Response>;
117
+ //#endregion
package/dist/og.mjs ADDED
@@ -0,0 +1,302 @@
1
+ //#region src/og.ts
2
+ /**
3
+ * Default font: Noto Sans from jsDelivr Fontsource CDN.
4
+ */
5
+ const NOTO_SANS = {
6
+ name: "Noto Sans",
7
+ regularUrl: "https://cdn.jsdelivr.net/fontsource/fonts/noto-sans@latest/latin-400-normal.ttf",
8
+ boldUrl: "https://cdn.jsdelivr.net/fontsource/fonts/noto-sans@latest/latin-700-normal.ttf"
9
+ };
10
+ const fontCaches = /* @__PURE__ */ new Map();
11
+ /**
12
+ * Fetches an ArrayBuffer with a timeout.
13
+ */
14
+ async function fetchArrayBuffer(url, timeoutMs = 5e3) {
15
+ const controller = new AbortController();
16
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
17
+ try {
18
+ const res = await fetch(url, { signal: controller.signal });
19
+ if (!res.ok) throw new Error(`Failed to fetch font from ${url}: status ${res.status}`);
20
+ return await res.arrayBuffer();
21
+ } finally {
22
+ clearTimeout(timeoutId);
23
+ }
24
+ }
25
+ /**
26
+ * Fetches and caches the regular + bold TTF buffers for the given font config. Uses a module-level
27
+ * cache keyed on `regularUrl`, so repeated calls within the same Worker lifetime are free after the
28
+ * first fetch.
29
+ */
30
+ async function loadOgFonts(config = NOTO_SANS, timeoutMs = 5e3) {
31
+ const cached = fontCaches.get(config.regularUrl);
32
+ if (cached) return cached;
33
+ const [regular, bold] = await Promise.all([fetchArrayBuffer(config.regularUrl, timeoutMs), fetchArrayBuffer(config.boldUrl, timeoutMs)]);
34
+ const result = {
35
+ regular,
36
+ bold
37
+ };
38
+ fontCaches.set(config.regularUrl, result);
39
+ return result;
40
+ }
41
+ /**
42
+ * Builds a takumi-js-compatible VNode for the default Rimelight OG layout.
43
+ *
44
+ * Consumers that need a fully custom look should build their own VNode and call
45
+ * `renderOgResponse()` directly -- this function is just the house style.
46
+ */
47
+ function defaultOgLayout(options, font = NOTO_SANS) {
48
+ const { title = "", description, brand, badge, date, background = "dark", preview = false } = options;
49
+ const MAX_TITLE_LEN = 160;
50
+ const MAX_DESC_LEN = 240;
51
+ const safeTitle = title.length > MAX_TITLE_LEN ? title.slice(0, 159) + "…" : title;
52
+ const safeDescription = description && description.length > MAX_DESC_LEN ? description.slice(0, 239) + "…" : description;
53
+ const bg = background === "dark" ? { backgroundColor: "#0a0a0a" } : background === "gradient" ? { background: "linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)" } : { background };
54
+ const isGradient = background !== "dark";
55
+ const brandNode = typeof brand === "object" && brand !== null ? brand : {
56
+ type: "span",
57
+ props: {
58
+ style: {
59
+ fontSize: "28px",
60
+ fontWeight: 700,
61
+ color: "#ffffff",
62
+ letterSpacing: "-0.02em"
63
+ },
64
+ children: brand ?? ""
65
+ }
66
+ };
67
+ const accentBar = isGradient ? {
68
+ type: "div",
69
+ props: { style: {
70
+ width: "60px",
71
+ height: "4px",
72
+ backgroundColor: "#60a5fa",
73
+ borderRadius: "2px",
74
+ marginBottom: "16px"
75
+ } }
76
+ } : null;
77
+ const titleFontSize = isGradient ? "52px" : "56px";
78
+ const descColor = isGradient ? "#94a3b8" : "#a0a0a0";
79
+ const descFontSize = isGradient ? "22px" : "24px";
80
+ const badgeNode = badge ? isGradient ? {
81
+ type: "div",
82
+ props: {
83
+ style: {
84
+ padding: "6px 16px",
85
+ borderRadius: "9999px",
86
+ backgroundColor: "rgba(96,165,250,0.15)",
87
+ border: "1px solid rgba(96,165,250,0.3)",
88
+ fontSize: "14px",
89
+ fontWeight: 600,
90
+ color: "#93c5fd"
91
+ },
92
+ children: badge
93
+ }
94
+ } : {
95
+ type: "div",
96
+ props: {
97
+ style: {
98
+ padding: "8px 20px",
99
+ borderRadius: "9999px",
100
+ border: "1px solid #333333",
101
+ fontSize: "16px",
102
+ fontWeight: 600,
103
+ color: "#e5e5e5"
104
+ },
105
+ children: badge
106
+ }
107
+ } : null;
108
+ const dateNode = date ? {
109
+ type: "div",
110
+ props: {
111
+ style: {
112
+ fontSize: "16px",
113
+ color: "#666666"
114
+ },
115
+ children: date
116
+ }
117
+ } : null;
118
+ const previewOverlay = preview ? {
119
+ type: "div",
120
+ props: { style: {
121
+ position: "absolute",
122
+ inset: 0,
123
+ border: "6px dashed #ff4444",
124
+ pointerEvents: "none"
125
+ } }
126
+ } : null;
127
+ const previewLabel = preview ? {
128
+ type: "div",
129
+ props: {
130
+ style: {
131
+ position: "absolute",
132
+ top: "12px",
133
+ right: "12px",
134
+ backgroundColor: "#ff4444",
135
+ color: "#ffffff",
136
+ fontSize: "14px",
137
+ fontWeight: 700,
138
+ padding: "6px 16px",
139
+ borderRadius: "4px",
140
+ letterSpacing: "0.05em"
141
+ },
142
+ children: "PREVIEW"
143
+ }
144
+ } : null;
145
+ return {
146
+ type: "div",
147
+ props: {
148
+ style: {
149
+ width: "100%",
150
+ height: "100%",
151
+ display: "flex",
152
+ flexDirection: "column",
153
+ padding: "56px",
154
+ color: "#e5e5e5",
155
+ fontFamily: font.name,
156
+ position: "relative",
157
+ overflow: "hidden",
158
+ ...bg
159
+ },
160
+ children: [
161
+ {
162
+ type: "div",
163
+ props: {
164
+ style: {
165
+ display: "flex",
166
+ alignItems: "center"
167
+ },
168
+ children: [brandNode]
169
+ }
170
+ },
171
+ {
172
+ type: "div",
173
+ props: {
174
+ style: {
175
+ display: "flex",
176
+ flexDirection: "column",
177
+ justifyContent: "center",
178
+ flexGrow: 1,
179
+ paddingTop: "24px"
180
+ },
181
+ children: [
182
+ accentBar,
183
+ {
184
+ type: "div",
185
+ props: {
186
+ style: {
187
+ fontSize: titleFontSize,
188
+ fontWeight: 700,
189
+ color: "#ffffff",
190
+ lineHeight: 1.15,
191
+ maxWidth: "950px"
192
+ },
193
+ children: safeTitle
194
+ }
195
+ },
196
+ safeDescription ? {
197
+ type: "div",
198
+ props: {
199
+ style: {
200
+ fontSize: descFontSize,
201
+ fontWeight: 400,
202
+ color: descColor,
203
+ marginTop: isGradient ? "12px" : "16px",
204
+ lineHeight: 1.4,
205
+ maxWidth: isGradient ? "800px" : "850px"
206
+ },
207
+ children: safeDescription
208
+ }
209
+ } : null
210
+ ].filter(Boolean)
211
+ }
212
+ },
213
+ {
214
+ type: "div",
215
+ props: {
216
+ style: {
217
+ display: "flex",
218
+ alignItems: "center",
219
+ justifyContent: isGradient ? "flex-start" : "space-between",
220
+ gap: "12px",
221
+ marginTop: "auto"
222
+ },
223
+ children: [badgeNode, dateNode].filter(Boolean)
224
+ }
225
+ },
226
+ previewOverlay,
227
+ previewLabel
228
+ ].filter(Boolean)
229
+ }
230
+ };
231
+ }
232
+ /**
233
+ * Renders any takumi-js VNode to a PNG `Response` with standard OG headers. Use this when you
234
+ * supply your own layout entirely.
235
+ *
236
+ * @example
237
+ * const vnode = myBrandedLayout({ title, description })
238
+ * return renderOgResponse(vnode, { font: MY_FONT })
239
+ */
240
+ async function renderOgResponse(vnode, config = {}) {
241
+ const { font = NOTO_SANS, width = 1200, height = 630 } = config;
242
+ try {
243
+ const { render } = await import("takumi-js");
244
+ const { regular, bold } = await loadOgFonts(font);
245
+ const pngBuffer = await render(vnode, {
246
+ width,
247
+ height,
248
+ fonts: [{
249
+ name: font.name,
250
+ data: regular,
251
+ weight: 400,
252
+ style: "normal"
253
+ }, {
254
+ name: font.name,
255
+ data: bold,
256
+ weight: 700,
257
+ style: "normal"
258
+ }]
259
+ });
260
+ return new Response(new Blob([new Uint8Array(pngBuffer)], { type: "image/png" }), {
261
+ status: 200,
262
+ headers: {
263
+ "Content-Type": "image/png",
264
+ "Cache-Control": "public, max-age=31536000, immutable",
265
+ "CDN-Cache-Control": "public, max-age=31536000"
266
+ }
267
+ });
268
+ } catch (error) {
269
+ console.error("[OG Render Error]:", error);
270
+ return new Response(JSON.stringify({
271
+ error: "Failed to render Open Graph image",
272
+ message: error instanceof Error ? error.message : String(error)
273
+ }), {
274
+ status: 500,
275
+ headers: {
276
+ "Content-Type": "application/json",
277
+ "Cache-Control": "no-store"
278
+ }
279
+ });
280
+ }
281
+ }
282
+ /**
283
+ * Renders the default OG layout to a PNG `Response`. The 90% case -- use `renderOgResponse()` for
284
+ * full layout control.
285
+ *
286
+ * @example
287
+ * export const GET: APIRoute = async ({ request }) => {
288
+ * const url = new URL(request.url)
289
+ * return renderDefaultOg({
290
+ * title: url.searchParams.get("title") ?? "Untitled",
291
+ * description: url.searchParams.get("description") ?? "",
292
+ * brand: "My Site",
293
+ * badge: url.searchParams.get("type") ?? ""
294
+ * })
295
+ * }
296
+ */
297
+ async function renderDefaultOg(options, config = {}) {
298
+ const { font = NOTO_SANS } = config;
299
+ return renderOgResponse(defaultOgLayout(options, font), config);
300
+ }
301
+ //#endregion
302
+ export { NOTO_SANS, defaultOgLayout, loadOgFonts, renderDefaultOg, renderOgResponse };
package/dist/robots.d.mts CHANGED
@@ -1,8 +1,7 @@
1
- import { o as RobotsOptions } from "./types-e7gqYZwc.mjs";
1
+ import { RobotsOptions } from "./types.mjs";
2
2
  //#region src/robots.d.ts
3
3
  /**
4
4
  * Build robots.txt content from options
5
5
  */
6
- declare function buildRobotsTxt(options: RobotsOptions): string;
7
- //#endregion
8
- export { buildRobotsTxt };
6
+ export declare function buildRobotsTxt(options: RobotsOptions): string;
7
+ //#endregion
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/llms-full.txt.d.ts
3
+ export declare const prerender = false;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,57 @@
1
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
2
+ import * as dbMod from "virtual:rimelight-seo/db";
3
+ import * as corpusMod from "virtual:rimelight-seo/corpus";
4
+ //#region src/routes/llms-full.txt.ts
5
+ const prerender = false;
6
+ const GET = async ({ site, url }) => {
7
+ const siteConfig = siteConfigMod.siteConfig || {};
8
+ const siteUrl = site ? site.toString() : siteConfig.url || `${url.protocol}//${url.host}`;
9
+ const title = siteConfig.name ? `${siteConfig.name} Full Documentation Corpus` : "Full Documentation Corpus";
10
+ const description = "Complete single-corpus documentation for AI agents and LLM ingest.";
11
+ if (Array.isArray(corpusMod.corpusPages) && corpusMod.corpusPages.length > 0) {
12
+ const sections = /* @__PURE__ */ new Map();
13
+ for (const p of corpusMod.corpusPages) {
14
+ const key = (p.section || "GENERAL").toUpperCase();
15
+ if (!sections.has(key)) sections.set(key, []);
16
+ const slug = p.slug === "index" ? "" : p.slug;
17
+ const pageUrl = `${siteUrl.replace(/\/$/, "")}/${slug}`.replace(/\/+$/, "");
18
+ sections.get(key).push(`### ${p.title}\nURL: ${pageUrl}\n${p.description || ""}`);
19
+ }
20
+ const body = [
21
+ `# ${title}`,
22
+ "",
23
+ description,
24
+ "",
25
+ ...Array.from(sections.entries()).flatMap(([section, pages]) => [
26
+ `## ${section}`,
27
+ "",
28
+ ...pages,
29
+ ""
30
+ ])
31
+ ].join("\n");
32
+ return new Response(body, { headers: {
33
+ "Content-Type": "text/plain; charset=utf-8",
34
+ "Cache-Control": "public, max-age=3600, s-maxage=86400"
35
+ } });
36
+ }
37
+ if (dbMod.db) try {
38
+ const { renderCorpusMarkdown } = await import("@rimelight/cms");
39
+ const body = await renderCorpusMarkdown(dbMod.db, {
40
+ type: "doc",
41
+ locale: "en",
42
+ siteUrl,
43
+ title,
44
+ description
45
+ });
46
+ return new Response(body, { headers: {
47
+ "Content-Type": "text/plain; charset=utf-8",
48
+ "Cache-Control": "public, max-age=3600, s-maxage=86400"
49
+ } });
50
+ } catch {}
51
+ return new Response(`# ${title}\n\n${description}\n`, { headers: {
52
+ "Content-Type": "text/plain; charset=utf-8",
53
+ "Cache-Control": "public, max-age=3600, s-maxage=86400"
54
+ } });
55
+ };
56
+ //#endregion
57
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/llms.txt.d.ts
3
+ export declare const prerender = false;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,68 @@
1
+ import { buildLlmsTxt } from "../llms.mjs";
2
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
3
+ import * as dbMod from "virtual:rimelight-seo/db";
4
+ import * as corpusMod from "virtual:rimelight-seo/corpus";
5
+ import seoOptions from "virtual:rimelight-seo/options";
6
+ //#region src/routes/llms.txt.ts
7
+ const prerender = false;
8
+ const GET = async ({ site, url }) => {
9
+ const siteConfig = siteConfigMod.siteConfig || {};
10
+ const siteUrl = site ? site.toString() : siteConfig.url || `${url.protocol}//${url.host}`;
11
+ const llmsOpts = typeof seoOptions?.llms === "object" ? seoOptions.llms : {};
12
+ let pages = [];
13
+ if (Array.isArray(llmsOpts.pages) && llmsOpts.pages.length > 0) pages = llmsOpts.pages;
14
+ else if (Array.isArray(corpusMod.corpusPages) && corpusMod.corpusPages.length > 0) pages = corpusMod.corpusPages.map((p) => {
15
+ const slug = p.slug === "index" ? "" : p.slug;
16
+ const pageUrl = `${siteUrl.replace(/\/$/, "")}/${slug}`.replace(/\/+$/, "");
17
+ return {
18
+ title: p.title,
19
+ description: p.description,
20
+ url: pageUrl,
21
+ section: p.section ? p.section.toUpperCase() : "GENERAL"
22
+ };
23
+ });
24
+ else if (dbMod.db) try {
25
+ const { pages: pagesTable } = await import("#db/schema").catch(() => ({ pages: null }));
26
+ const { and, isNotNull, isNull } = await import("drizzle-orm");
27
+ if (pagesTable) {
28
+ const docRows = await dbMod.db.select().from(pagesTable).where(and(isNull(pagesTable.deletedAt), isNotNull(pagesTable.publishedVersionId))).catch(() => []);
29
+ const resolveLocalized = (val) => {
30
+ if (typeof val === "string") return val;
31
+ if (typeof val === "object" && val !== null) {
32
+ const rec = val;
33
+ return rec["en"] || Object.values(rec)[0] || "";
34
+ }
35
+ return typeof val === "number" ? String(val) : "";
36
+ };
37
+ pages = docRows.map((p) => {
38
+ const title = resolveLocalized(p.title) || p.slug;
39
+ const description = resolveLocalized(p.description);
40
+ const slug = p.slug === "index" ? "" : p.slug;
41
+ const pathPrefix = p.type === "doc" ? "docs" : p.type || "docs";
42
+ const pageUrl = `${siteUrl.replace(/\/$/, "")}/en/${pathPrefix}/${slug}`.replace(/\/+$/, "");
43
+ return {
44
+ title,
45
+ description,
46
+ url: pageUrl,
47
+ markdownUrl: `${pageUrl}.md`,
48
+ section: p.type ? p.type.toUpperCase() : "GENERAL"
49
+ };
50
+ });
51
+ }
52
+ } catch {}
53
+ const title = llmsOpts.title || siteConfig.name || "Documentation";
54
+ const description = llmsOpts.description || siteConfig.description || "Documentation index for AI agents.";
55
+ const body = buildLlmsTxt({
56
+ site: siteUrl,
57
+ title,
58
+ description,
59
+ pages,
60
+ ...llmsOpts
61
+ });
62
+ return new Response(body, { headers: {
63
+ "Content-Type": "text/plain; charset=utf-8",
64
+ "Cache-Control": "public, max-age=3600, s-maxage=86400"
65
+ } });
66
+ };
67
+ //#endregion
68
+ export { GET, prerender };
@@ -0,0 +1,4 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/robots.txt.d.ts
3
+ export declare const GET: APIRoute;
4
+ //#endregion
@@ -0,0 +1,46 @@
1
+ import { buildRobotsTxt } from "../robots.mjs";
2
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
3
+ import seoOptions from "virtual:rimelight-seo/options";
4
+ import * as seoConfig from "virtual:rimelight-seo/config";
5
+ //#region src/routes/robots.txt.ts
6
+ const GET = ({ site, url }) => {
7
+ const siteUrl = site ? site.toString() : siteConfigMod.siteConfig?.url || url.origin;
8
+ const sitemapURL = new URL("sitemap.xml", siteUrl).toString();
9
+ const defaultPrivatePrefixes = [
10
+ "/dashboard",
11
+ "/admin",
12
+ "/cms",
13
+ "/internal",
14
+ "/api",
15
+ "/dev",
16
+ "/og",
17
+ "/open-graph",
18
+ "/auth"
19
+ ];
20
+ const customPrivatePrefixes = Array.isArray(seoConfig.PRIVATE_PATH_PREFIXES) ? seoConfig.PRIVATE_PATH_PREFIXES : [];
21
+ const privatePrefixes = Array.from(/* @__PURE__ */ new Set([...defaultPrivatePrefixes, ...customPrivatePrefixes]));
22
+ const disallowPatterns = [...privatePrefixes, ...privatePrefixes.map((p) => `/*${p.startsWith("/") ? p : `/${p}`}/`)];
23
+ const robotsOpts = typeof seoOptions?.robots === "object" ? seoOptions.robots : {};
24
+ const defaultAgentRules = [{
25
+ userAgent: "GPTBot",
26
+ disallow: privatePrefixes.map((p) => `${p.startsWith("/") ? p : `/${p}`}/`),
27
+ allow: ["/"]
28
+ }, {
29
+ userAgent: "ClaudeBot",
30
+ disallow: privatePrefixes.map((p) => `${p.startsWith("/") ? p : `/${p}`}/`),
31
+ allow: ["/"]
32
+ }];
33
+ const robots = buildRobotsTxt({
34
+ sitemapUrl: sitemapURL,
35
+ disallow: disallowPatterns,
36
+ allow: ["/"],
37
+ userAgentRules: defaultAgentRules,
38
+ ...robotsOpts
39
+ });
40
+ return new Response(robots, { headers: {
41
+ "Content-Type": "text/plain; charset=utf-8",
42
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
43
+ } });
44
+ };
45
+ //#endregion
46
+ export { GET };
@@ -0,0 +1,4 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/rss.xml.d.ts
3
+ export declare const GET: APIRoute;
4
+ //#endregion
@@ -0,0 +1,51 @@
1
+ import { buildRssXml } from "../rss.mjs";
2
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
3
+ import * as dbMod from "virtual:rimelight-seo/db";
4
+ import seoOptions from "virtual:rimelight-seo/options";
5
+ import * as seoConfig from "virtual:rimelight-seo/config";
6
+ //#region src/routes/rss.xml.ts
7
+ const GET = async ({ site, url }) => {
8
+ const rssOpts = typeof seoOptions?.rss === "object" ? seoOptions.rss : {};
9
+ const siteConfig = siteConfigMod.siteConfig || {};
10
+ const siteUrl = site ? site.toString() : rssOpts.site || siteConfig.url || url.origin;
11
+ let items = [];
12
+ if (Array.isArray(rssOpts.items)) items = rssOpts.items;
13
+ else if (typeof seoConfig.rssEntries === "function") items = await seoConfig.rssEntries();
14
+ else if (dbMod.db) try {
15
+ const { pages } = await import("#db/schema").catch(() => ({ pages: null }));
16
+ const { and, isNull, sql } = await import("drizzle-orm");
17
+ if (pages) items = (await dbMod.db.select().from(pages).where(and(sql`${pages.type} = 'blog'`, isNull(pages.deletedAt))).catch(() => [])).map((p) => {
18
+ return {
19
+ title: typeof p.title === "string" ? p.title : p.title?.en || p.title && Object.values(p.title)[0] || p.slug,
20
+ description: typeof p.description === "string" ? p.description : p.description?.en || p.description && Object.values(p.description)[0] || "",
21
+ link: `/blog/${p.slug}`,
22
+ pubDate: p.postedAt || p.createdAt || /* @__PURE__ */ new Date()
23
+ };
24
+ });
25
+ } catch {}
26
+ if (items.length === 0) try {
27
+ const { getCollection } = await import("astro:content");
28
+ const blog = await getCollection("blog");
29
+ if (Array.isArray(blog)) items = blog.map((post) => ({
30
+ title: post.data?.title || post.id,
31
+ pubDate: post.data?.pubDate || /* @__PURE__ */ new Date(),
32
+ description: post.data?.description,
33
+ link: `/blog/${post.id}/`
34
+ }));
35
+ } catch {}
36
+ const feedTitle = rssOpts.title || siteConfig.name || "RSS Feed";
37
+ const feedDesc = rssOpts.description || siteConfig.description || "";
38
+ const xml = buildRssXml({
39
+ title: feedTitle,
40
+ description: feedDesc,
41
+ site: siteUrl,
42
+ language: rssOpts.language || "en-US",
43
+ items
44
+ });
45
+ return new Response(xml, { headers: {
46
+ "Content-Type": "application/xml; charset=utf-8",
47
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
48
+ } });
49
+ };
50
+ //#endregion
51
+ export { GET };
@@ -0,0 +1,4 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/sitemap.xml.d.ts
3
+ export declare const GET: APIRoute;
4
+ //#endregion