@rimelight/seo 0.0.7 → 0.0.9

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/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { generateIco, generateMaskablePng, generateOgCard, generatePng } from "./assets.mjs";
2
2
  import { i as buildBreadcrumbSchema, n as BreadcrumbItem, r as buildArticleSchema, t as ArticleSchemaOptions } from "./schema-C8v69blQ.mjs";
3
3
  import { a as buildLlmsTxt, i as buildLlmsFullTxt, n as LlmsPage, r as LlmsTxtOptions, t as LlmsFullTxtOptions } from "./llms-BVLxzjr-.mjs";
4
- import { ApiCatalog, ApiCatalogItem, ApiCatalogOptions, ApiCatalogRouteOptions, AssetRouteOptions, ChangeFreq, HeadProps, LlmsFullRouteOptions, LlmsRouteOptions, LocaleConfig, ManifestRouteOptions, OGImage, OpenGraphMeta, RSSFeed, RimelightSeoOptions, RobotsOptions, RobotsRouteOptions, RssCollectionConfig, RssFeedOptions, RssItem, RssOptions, SecurityRouteOptions, SecurityTxtOptions, SeoEntry, SiteConfig, SitemapAlternate, SitemapOptions, SitemapUrl, TwitterMeta, UserAgentRule, WebManifest, WebManifestIcon, WebManifestOptions, WebManifestShortcut } from "./types.mjs";
4
+ import { ApiCatalog, ApiCatalogItem, ApiCatalogOptions, ApiCatalogRouteOptions, AssetRouteOptions, ChangeFreq, HeadProps, LlmsFullRouteOptions, LlmsRouteOptions, LocaleConfig, ManifestRouteOptions, OGImage, OgRouteOptions, OpenGraphMeta, RSSFeed, RimelightSeoOptions, RobotsOptions, RobotsRouteOptions, RssCollectionConfig, RssFeedOptions, RssItem, RssOptions, SecurityRouteOptions, SecurityTxtOptions, SeoEntry, SiteConfig, SitemapAlternate, SitemapOptions, SitemapUrl, TwitterMeta, UserAgentRule, WebManifest, WebManifestIcon, WebManifestOptions, WebManifestShortcut } from "./types.mjs";
5
5
  import { buildApiCatalog } from "./catalog.mjs";
6
6
  import { buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, chunkSitemapUrls } from "./sitemap.mjs";
7
7
  import { buildRobotsTxt } from "./robots.mjs";
@@ -11,4 +11,4 @@ import { buildSecurityTxt } from "./security.mjs";
11
11
  import { buildWebManifest } from "./manifest.mjs";
12
12
  import { OgFontConfig, OgLayoutOptions, OgRenderConfig } from "./og.mjs";
13
13
  import rimelightSeo from "./integration.mjs";
14
- export { type ApiCatalog, type ApiCatalogItem, type ApiCatalogOptions, type ApiCatalogRouteOptions, type ArticleSchemaOptions, type AssetRouteOptions, type BreadcrumbItem, type ChangeFreq, type HeadProps, type LlmsFullRouteOptions, type LlmsFullTxtOptions, type LlmsPage, type LlmsRouteOptions, type LlmsTxtOptions, type LocaleConfig, type ManifestRouteOptions, type OGImage, type OgFontConfig, type OgLayoutOptions, type OgRenderConfig, type OpenGraphMeta, type RSSFeed, type RimelightSeoOptions, type RobotsOptions, type RobotsRouteOptions, type RssCollectionConfig, type RssFeedOptions, type RssItem, type RssOptions, type SecurityRouteOptions, type SecurityTxtOptions, type SeoEntry, type SiteConfig, type SitemapAlternate, type SitemapOptions, type SitemapUrl, type TwitterMeta, type UserAgentRule, type WebManifest, type WebManifestIcon, type WebManifestOptions, type WebManifestShortcut, buildApiCatalog, buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSecurityTxt, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebManifest, buildWebSiteSchema, chunkSitemapUrls, deepMerge, rimelightSeo as default, rimelightSeo, escapeXml, generateIco, generateMaskablePng, generateOgCard, generatePng };
14
+ export { type ApiCatalog, type ApiCatalogItem, type ApiCatalogOptions, type ApiCatalogRouteOptions, type ArticleSchemaOptions, type AssetRouteOptions, type BreadcrumbItem, type ChangeFreq, type HeadProps, type LlmsFullRouteOptions, type LlmsFullTxtOptions, type LlmsPage, type LlmsRouteOptions, type LlmsTxtOptions, type LocaleConfig, type ManifestRouteOptions, type OGImage, type OgFontConfig, type OgLayoutOptions, type OgRenderConfig, type OgRouteOptions, type OpenGraphMeta, type RSSFeed, type RimelightSeoOptions, type RobotsOptions, type RobotsRouteOptions, type RssCollectionConfig, type RssFeedOptions, type RssItem, type RssOptions, type SecurityRouteOptions, type SecurityTxtOptions, type SeoEntry, type SiteConfig, type SitemapAlternate, type SitemapOptions, type SitemapUrl, type TwitterMeta, type UserAgentRule, type WebManifest, type WebManifestIcon, type WebManifestOptions, type WebManifestShortcut, buildApiCatalog, buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSecurityTxt, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebManifest, buildWebSiteSchema, chunkSitemapUrls, deepMerge, rimelightSeo as default, rimelightSeo, escapeXml, generateIco, generateMaskablePng, generateOgCard, generatePng };
@@ -15,16 +15,35 @@ function rimelightSeo(options = {}) {
15
15
  }
16
16
  return null;
17
17
  };
18
- const seoConfigPath = findFile([
19
- "src/config/seo.config.ts",
20
- "src/config/seo.config.js",
21
- "src/config/seo.config.mjs"
22
- ]);
23
- const siteConfigPath = findFile([
24
- "src/config/site.config.ts",
25
- "src/config/site.config.js",
26
- "src/config/site.config.mjs"
27
- ]);
18
+ const siteOpt = options.site || {};
19
+ const normalizedSiteConfig = {
20
+ id: siteOpt.id || options.id || "",
21
+ name: siteOpt.name || options.name || "",
22
+ description: siteOpt.description || options.description || "",
23
+ url: siteOpt.url || options.url || "",
24
+ ogImage: siteOpt.ogImage || options.ogImage || void 0,
25
+ author: siteOpt.author || options.author || siteOpt.name || options.name || "",
26
+ email: siteOpt.email || options.email || "",
27
+ branding: {
28
+ logo: { alt: siteOpt.branding?.logo?.alt || options.branding?.logo?.alt || siteOpt.name || options.name || "" },
29
+ colors: {
30
+ themeColor: siteOpt.branding?.colors?.themeColor || options.branding?.colors?.themeColor || "#ffffff",
31
+ backgroundColor: siteOpt.branding?.colors?.backgroundColor || options.branding?.colors?.backgroundColor || "#ffffff"
32
+ },
33
+ ...siteOpt.branding,
34
+ ...options.branding
35
+ },
36
+ seo: {
37
+ titleTemplate: siteOpt.seo?.titleTemplate || options.titleTemplate || (options.name ? `%s | ${options.name}` : "%s"),
38
+ maxDescriptionLength: siteOpt.seo?.maxDescriptionLength || options.maxDescriptionLength || 160,
39
+ authorHandle: siteOpt.seo?.authorHandle || options.authorHandle || void 0,
40
+ ...siteOpt.seo
41
+ },
42
+ ...siteOpt.openGraph || options.openGraph ? { openGraph: siteOpt.openGraph || options.openGraph } : {},
43
+ ...siteOpt.twitter || options.twitter ? { twitter: siteOpt.twitter || options.twitter } : {}
44
+ };
45
+ const seoLocales = options.locales;
46
+ const privatePathPrefixes = options.privatePathPrefixes || [];
28
47
  const dbPath = findFile([
29
48
  "src/db/index.ts",
30
49
  "src/db/index.js",
@@ -58,10 +77,16 @@ function rimelightSeo(options = {}) {
58
77
  pattern: typeof options.robots === "object" && options.robots.path ? options.robots.path : "/robots.txt",
59
78
  entrypoint: "@rimelight/seo/routes/robots.txt.ts"
60
79
  });
61
- if (options.sitemap !== false) injectRoute({
62
- pattern: typeof options.sitemap === "object" && options.sitemap.path ? options.sitemap.path : "/sitemap.xml",
63
- entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
64
- });
80
+ if (options.sitemap !== false) {
81
+ injectRoute({
82
+ pattern: typeof options.sitemap === "object" && options.sitemap.path ? options.sitemap.path : "/sitemap.xml",
83
+ entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
84
+ });
85
+ injectRoute({
86
+ pattern: "/sitemap-index.xml",
87
+ entrypoint: "@rimelight/seo/routes/sitemap-index.xml.ts"
88
+ });
89
+ }
65
90
  if (options.rss !== false) injectRoute({
66
91
  pattern: typeof options.rss === "object" && options.rss.path ? options.rss.path : "/rss.xml",
67
92
  entrypoint: "@rimelight/seo/routes/rss.xml.ts"
@@ -93,6 +118,10 @@ function rimelightSeo(options = {}) {
93
118
  pattern: typeof options.manifest === "object" && options.manifest.path ? options.manifest.path : "/site.webmanifest",
94
119
  entrypoint: "@rimelight/seo/routes/site.webmanifest.ts"
95
120
  });
121
+ if (options.og !== false) injectRoute({
122
+ pattern: typeof options.og === "object" && options.og.path ? options.og.path : "/open-graph/[...route].png",
123
+ entrypoint: "@rimelight/seo/routes/open-graph.[...route].png.ts"
124
+ });
96
125
  updateConfig({ vite: { plugins: [{
97
126
  name: "vite-plugin-rimelight-seo-assets",
98
127
  configureServer(server) {
@@ -240,6 +269,7 @@ function rimelightSeo(options = {}) {
240
269
  name: "vite-plugin-rimelight-seo",
241
270
  resolveId(id) {
242
271
  if (id === "virtual:rimelight-seo-config" || id === "virtual:rimelight-seo/options") return "\0virtual:rimelight-seo/options";
272
+ if (id === "virtual:rimelight/seo") return "\0virtual:rimelight/seo";
243
273
  if (id === "virtual:rimelight-seo/config") return "\0virtual:rimelight-seo/config";
244
274
  if (id === "virtual:rimelight-seo/site-config") return "\0virtual:rimelight-seo/site-config";
245
275
  if (id === "virtual:rimelight-seo/db") return "\0virtual:rimelight-seo/db";
@@ -248,22 +278,21 @@ function rimelightSeo(options = {}) {
248
278
  },
249
279
  load(id) {
250
280
  if (id === "\0virtual:rimelight-seo/options") return `export default ${JSON.stringify(options)};`;
251
- if (id === "\0virtual:rimelight-seo/config") {
252
- if (seoConfigPath) return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
253
- export const SEO_LOCALES = customConfig.SEO_LOCALES;
254
- export const PRIVATE_PATH_PREFIXES = customConfig.PRIVATE_PATH_PREFIXES || [];
255
- export const seoEntries = customConfig.seoEntries;
256
- export const rssEntries = customConfig.rssEntries;
257
- export default customConfig;`;
258
- return `export const SEO_LOCALES = undefined;
259
- export const PRIVATE_PATH_PREFIXES = [];
281
+ if (id === "\0virtual:rimelight/seo") return `export const siteConfig = ${JSON.stringify(normalizedSiteConfig)};
282
+ export const SEO_LOCALES = ${JSON.stringify(seoLocales)};
283
+ export const PRIVATE_PATH_PREFIXES = ${JSON.stringify(privatePathPrefixes)};
284
+ export const seoConfig = {
285
+ SEO_LOCALES: ${JSON.stringify(seoLocales)},
286
+ PRIVATE_PATH_PREFIXES: ${JSON.stringify(privatePathPrefixes)}
287
+ };
288
+ export default siteConfig;`;
289
+ if (id === "\0virtual:rimelight-seo/config") return `export const SEO_LOCALES = ${JSON.stringify(seoLocales)};
290
+ export const PRIVATE_PATH_PREFIXES = ${JSON.stringify(privatePathPrefixes)};
260
291
  export const seoEntries = async () => [];
261
- export const rssEntries = undefined;`;
262
- }
263
- if (id === "\0virtual:rimelight-seo/site-config") {
264
- if (siteConfigPath) return `export * from ${JSON.stringify(siteConfigPath)};`;
265
- return `export const siteConfig = { name: "Site", description: "", url: "" };`;
266
- }
292
+ export const rssEntries = undefined;
293
+ export default { SEO_LOCALES: ${JSON.stringify(seoLocales)}, PRIVATE_PATH_PREFIXES: ${JSON.stringify(privatePathPrefixes)} };`;
294
+ if (id === "\0virtual:rimelight-seo/site-config") return `export const siteConfig = ${JSON.stringify(normalizedSiteConfig)};
295
+ export default siteConfig;`;
267
296
  if (id === "\0virtual:rimelight-seo/db") {
268
297
  if (dbPath) return `export * from ${JSON.stringify(dbPath)};`;
269
298
  return `export const db = null;
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/open-graph.[...route].png.d.ts
3
+ export declare const prerender = false;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,57 @@
1
+ import { renderDefaultOg } from "../og.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ import * as dbMod from "virtual:rimelight-seo/db";
5
+ //#region src/routes/open-graph.[...route].png.ts
6
+ const prerender = false;
7
+ function getLocalizedText(val, locale = "en") {
8
+ if (typeof val === "object" && val !== null) {
9
+ const record = val;
10
+ return record[locale] || Object.values(record)[0] || "";
11
+ }
12
+ if (typeof val === "string") return val;
13
+ if (typeof val === "number" || typeof val === "boolean") return String(val);
14
+ return "";
15
+ }
16
+ const GET = async ({ request }) => {
17
+ const url = new URL(request.url);
18
+ const routeParam = url.pathname.replace(/^\/open-graph\//, "").replace(/\.png$/, "");
19
+ const siteConfig = siteConfigMod.siteConfig || {};
20
+ const ogOpts = typeof seoOptions?.og === "object" ? seoOptions.og : {};
21
+ const brand = ogOpts.brand || seoOptions?.name || siteConfig.name || url.hostname;
22
+ const defaultBg = ogOpts.defaultBackground ?? "dark";
23
+ const docsBg = ogOpts.docsBackground ?? "gradient";
24
+ let title = url.searchParams.get("title") || "";
25
+ let description = url.searchParams.get("description") || "";
26
+ let date = url.searchParams.get("pubDate") || "";
27
+ const badge = url.searchParams.get("type") || (routeParam.includes("blog") ? "Blog Post" : routeParam.includes("legal") ? "Legal" : "");
28
+ if (!title && routeParam && routeParam !== "page" && routeParam !== "default" && dbMod.db) try {
29
+ const { pages: pagesTable } = await import("#db/schema").catch(() => ({ pages: null }));
30
+ const { eq, and, isNull } = await import("drizzle-orm");
31
+ if (pagesTable) {
32
+ const slug = routeParam.split("/").pop() ?? "";
33
+ const page = await dbMod.db.select({
34
+ title: pagesTable.title,
35
+ description: pagesTable.description,
36
+ postedAt: pagesTable.postedAt
37
+ }).from(pagesTable).where(and(eq(pagesTable.slug, slug), isNull(pagesTable.deletedAt))).limit(1).then((r) => r[0]).catch(() => void 0);
38
+ if (page) {
39
+ title = getLocalizedText(page.title);
40
+ description = description || getLocalizedText(page.description);
41
+ if (page.postedAt && !date) date = new Date(page.postedAt).toLocaleDateString();
42
+ }
43
+ }
44
+ } catch {}
45
+ const isDocs = url.searchParams.get("isDocs") === "true" || routeParam.includes("docs");
46
+ return renderDefaultOg({
47
+ title: title || brand,
48
+ description,
49
+ brand,
50
+ badge,
51
+ date,
52
+ background: isDocs ? docsBg : defaultBg,
53
+ preview: url.searchParams.get("preview") === "1"
54
+ });
55
+ };
56
+ //#endregion
57
+ export { GET, prerender };
@@ -0,0 +1,4 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/sitemap-index.xml.d.ts
3
+ export declare const GET: APIRoute;
4
+ //#endregion
@@ -0,0 +1,16 @@
1
+ import { buildSitemapIndexXml } from "../sitemap.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config";
4
+ //#region src/routes/sitemap-index.xml.ts
5
+ const GET = ({ site, url }) => {
6
+ const siteUrl = site ? site.toString() : siteConfigMod.siteConfig?.url || url.origin;
7
+ const sitemapOpts = typeof seoOptions?.sitemap === "object" ? seoOptions.sitemap : {};
8
+ const sitemapPattern = typeof sitemapOpts.path === "string" ? sitemapOpts.path : "/sitemap.xml";
9
+ const xml = buildSitemapIndexXml([new URL(sitemapPattern, siteUrl).toString()]);
10
+ return new Response(xml, { headers: {
11
+ "Content-Type": "application/xml; charset=utf-8",
12
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
13
+ } });
14
+ };
15
+ //#endregion
16
+ export { GET };
package/dist/types.d.mts CHANGED
@@ -493,6 +493,47 @@ export interface AssetRouteOptions {
493
493
  ogImage?: boolean;
494
494
  }
495
495
  export interface RimelightSeoOptions {
496
+ /**
497
+ * Site identifier / domain (e.g. "rimelight.com")
498
+ */
499
+ id?: string;
500
+ /**
501
+ * Primary site name (e.g. "Rimelight")
502
+ */
503
+ name?: string;
504
+ /**
505
+ * Default site description
506
+ */
507
+ description?: string;
508
+ /**
509
+ * Canonical origin URL (e.g. "https://rimelight.com")
510
+ */
511
+ url?: string;
512
+ ogImage?: string;
513
+ author?: string;
514
+ email?: string;
515
+ branding?: Partial<SiteConfig["branding"]>;
516
+ titleTemplate?: string;
517
+ maxDescriptionLength?: number;
518
+ authorHandle?: string;
519
+ openGraph?: OpenGraphMeta;
520
+ twitter?: TwitterMeta;
521
+ /**
522
+ * Full or partial SiteConfig object
523
+ */
524
+ site?: Partial<SiteConfig>;
525
+ /**
526
+ * Locale mappings for hreflang / sitemaps
527
+ */
528
+ locales?: LocaleConfig | Record<string, string>;
529
+ /**
530
+ * Path prefixes to exclude from sitemap and indexing
531
+ */
532
+ privatePathPrefixes?: string[];
533
+ /**
534
+ * Custom SEO entry generator function
535
+ */
536
+ entries?: () => Promise<SeoEntry[]> | SeoEntry[];
496
537
  /**
497
538
  * Sitemap configuration or false to disable automatic /sitemap.xml (default: true)
498
539
  */
@@ -532,6 +573,40 @@ export interface RimelightSeoOptions {
532
573
  * icons) from a single master icon (default: true)
533
574
  */
534
575
  assets?: boolean | AssetRouteOptions;
576
+ /**
577
+ * Open Graph image route configuration, or false to disable automatic /open-graph/[...route].png
578
+ * (default: true). When enabled, the route reads brand/background from seo.name and injects a
579
+ * handler that supports query-param overrides and optional DB slug lookup.
580
+ */
581
+ og?: boolean | OgRouteOptions;
582
+ }
583
+ /**
584
+ * Configuration for the auto-injected /open-graph/[...route].png route.
585
+ */
586
+ export interface OgRouteOptions {
587
+ /**
588
+ * Route pattern for the OG image handler.
589
+ *
590
+ * @default "/open-graph/[...route].png"
591
+ */
592
+ path?: string;
593
+ /**
594
+ * Brand name shown top-left of every OG image. Defaults to seo.name / site.name from integration
595
+ * config.
596
+ */
597
+ brand?: string;
598
+ /**
599
+ * Background style used for standard pages.
600
+ *
601
+ * @default "dark"
602
+ */
603
+ defaultBackground?: "dark" | "gradient" | (string & {});
604
+ /**
605
+ * Background style used when ?isDocs=true or the route segment contains "docs".
606
+ *
607
+ * @default "gradient"
608
+ */
609
+ docsBackground?: "dark" | "gradient" | (string & {});
535
610
  }
536
611
  //#endregion
537
612
  export type { ArticleSchemaOptions, BreadcrumbItem, LlmsFullTxtOptions, LlmsPage, LlmsTxtOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/seo",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's SEO Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -73,8 +73,8 @@
73
73
  "access": "public"
74
74
  },
75
75
  "devDependencies": {
76
- "@rimelight/config": "0.0.9",
77
- "astro": "7.3.2",
76
+ "@rimelight/config": "0.0.12",
77
+ "astro": "7.3.3",
78
78
  "sharp": "^0.35.4",
79
79
  "typescript": "6.0.3"
80
80
  },
@@ -92,7 +92,7 @@
92
92
  }
93
93
  },
94
94
  "engines": {
95
- "node": ">=26.8.2"
95
+ "node": ">=26.9.0"
96
96
  },
97
97
  "scripts": {
98
98
  "build": "vp pack",
package/src/env.d.ts CHANGED
@@ -1,56 +1,66 @@
1
-
2
- declare module "virtual:rimelight-seo-config" {
3
- const config: import("./integration.js").RimelightSeoOptions
4
- export default config
5
- }
6
-
7
- declare module "virtual:rimelight-seo/options" {
8
- const config: import("./integration.js").RimelightSeoOptions
9
- export default config
10
- }
11
-
12
- declare module "virtual:rimelight-seo/config" {
13
- export const SEO_LOCALES: import("./types.js").LocaleConfig | undefined
14
- export const PRIVATE_PATH_PREFIXES: string[] | undefined
15
- export const seoEntries: (() => Promise<import("./types.js").SeoEntry[]>) | undefined
16
- export const rssEntries: (() => Promise<import("./types.js").RssItem[]>) | undefined
17
- }
18
-
19
- declare module "virtual:rimelight-seo/site-config" {
20
- export const siteConfig: import("./types.js").SiteConfig | Record<string, any>
21
- }
22
-
23
- declare module "virtual:rimelight-seo/db" {
24
- export const db: any
25
- export const pages: any
26
- }
27
-
28
- declare module "virtual:rimelight-seo/corpus" {
29
- export const corpusPages: any[]
30
- }
31
-
32
- declare module "#db/schema" {
33
- export const pages: any
34
- }
35
-
36
- declare module "drizzle-orm" {
37
- export const and: (...args: any[]) => any
38
- export const isNull: (column: any) => any
39
- export const isNotNull: (column: any) => any
40
- export const sql: any
41
- export const eq: (column: any, value: any) => any
42
- }
43
-
44
- declare module "@rimelight/cms" {
45
- export const renderCorpusMarkdown: (db: any, options: any) => Promise<string>
46
- }
47
-
48
- declare module "astro:content" {
49
- export const getCollection: (collection: string) => Promise<any[]>
50
- }
51
-
52
- declare module "virtual:rimelight-seo/assets" {
53
- export const masterIconBuffer: Buffer | null
54
- export const isSvg: boolean
55
- export const backgroundColor: string
56
- }
1
+ declare module "virtual:rimelight/seo" {
2
+ export const siteConfig: import("./types.js").SiteConfig
3
+ export const SEO_LOCALES: import("./types.js").LocaleConfig | Record<string, string> | undefined
4
+ export const PRIVATE_PATH_PREFIXES: string[]
5
+ export const seoConfig: {
6
+ SEO_LOCALES?: import("./types.js").LocaleConfig | Record<string, string>
7
+ PRIVATE_PATH_PREFIXES: string[]
8
+ }
9
+ export default siteConfig
10
+ }
11
+
12
+ declare module "virtual:rimelight-seo-config" {
13
+ const config: import("./integration.js").RimelightSeoOptions
14
+ export default config
15
+ }
16
+
17
+ declare module "virtual:rimelight-seo/options" {
18
+ const config: import("./integration.js").RimelightSeoOptions
19
+ export default config
20
+ }
21
+
22
+ declare module "virtual:rimelight-seo/config" {
23
+ export const SEO_LOCALES: import("./types.js").LocaleConfig | undefined
24
+ export const PRIVATE_PATH_PREFIXES: string[] | undefined
25
+ export const seoEntries: (() => Promise<import("./types.js").SeoEntry[]>) | undefined
26
+ export const rssEntries: (() => Promise<import("./types.js").RssItem[]>) | undefined
27
+ }
28
+
29
+ declare module "virtual:rimelight-seo/site-config" {
30
+ export const siteConfig: import("./types.js").SiteConfig | Record<string, any>
31
+ }
32
+
33
+ declare module "virtual:rimelight-seo/db" {
34
+ export const db: any
35
+ export const pages: any
36
+ }
37
+
38
+ declare module "virtual:rimelight-seo/corpus" {
39
+ export const corpusPages: any[]
40
+ }
41
+
42
+ declare module "#db/schema" {
43
+ export const pages: any
44
+ }
45
+
46
+ declare module "drizzle-orm" {
47
+ export const and: (...args: any[]) => any
48
+ export const isNull: (column: any) => any
49
+ export const isNotNull: (column: any) => any
50
+ export const sql: any
51
+ export const eq: (column: any, value: any) => any
52
+ }
53
+
54
+ declare module "@rimelight/cms" {
55
+ export const renderCorpusMarkdown: (db: any, options: any) => Promise<string>
56
+ }
57
+
58
+ declare module "astro:content" {
59
+ export const getCollection: (collection: string) => Promise<any[]>
60
+ }
61
+
62
+ declare module "virtual:rimelight-seo/assets" {
63
+ export const masterIconBuffer: Buffer | null
64
+ export const isSvg: boolean
65
+ export const backgroundColor: string
66
+ }
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export { buildApiCatalog } from "./catalog.js"
66
66
  export { buildArticleSchema, buildBreadcrumbSchema } from "./schema.js"
67
67
  export { generatePng, generateMaskablePng, generateIco, generateOgCard } from "./assets.js"
68
68
  export type { OgFontConfig, OgLayoutOptions, OgRenderConfig } from "./og.js"
69
+ export type { OgRouteOptions } from "./types.js"
69
70
 
70
71
  import { rimelightSeo } from "./integration.js"
71
72
  export type { RimelightSeoOptions } from "./integration.js"
@@ -23,17 +23,57 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
23
23
  return null
24
24
  }
25
25
 
26
- const seoConfigPath = findFile([
27
- "src/config/seo.config.ts",
28
- "src/config/seo.config.js",
29
- "src/config/seo.config.mjs"
30
- ])
26
+ const siteOpt = options.site || {}
27
+ const normalizedSiteConfig = {
28
+ id: siteOpt.id || options.id || "",
29
+ name: siteOpt.name || options.name || "",
30
+ description: siteOpt.description || options.description || "",
31
+ url: siteOpt.url || options.url || "",
32
+ ogImage: siteOpt.ogImage || options.ogImage || undefined,
33
+ author: siteOpt.author || options.author || siteOpt.name || options.name || "",
34
+ email: siteOpt.email || options.email || "",
35
+ branding: {
36
+ logo: {
37
+ alt:
38
+ siteOpt.branding?.logo?.alt ||
39
+ options.branding?.logo?.alt ||
40
+ siteOpt.name ||
41
+ options.name ||
42
+ ""
43
+ },
44
+ colors: {
45
+ themeColor:
46
+ siteOpt.branding?.colors?.themeColor ||
47
+ options.branding?.colors?.themeColor ||
48
+ "#ffffff",
49
+ backgroundColor:
50
+ siteOpt.branding?.colors?.backgroundColor ||
51
+ options.branding?.colors?.backgroundColor ||
52
+ "#ffffff"
53
+ },
54
+ ...siteOpt.branding,
55
+ ...options.branding
56
+ },
57
+ seo: {
58
+ titleTemplate:
59
+ siteOpt.seo?.titleTemplate ||
60
+ options.titleTemplate ||
61
+ (options.name ? `%s | ${options.name}` : "%s"),
62
+ maxDescriptionLength:
63
+ siteOpt.seo?.maxDescriptionLength || options.maxDescriptionLength || 160,
64
+ authorHandle: siteOpt.seo?.authorHandle || options.authorHandle || undefined,
65
+ ...siteOpt.seo
66
+ },
67
+ ...(siteOpt.openGraph || options.openGraph
68
+ ? { openGraph: siteOpt.openGraph || options.openGraph }
69
+ : {}),
70
+ ...(siteOpt.twitter || options.twitter
71
+ ? { twitter: siteOpt.twitter || options.twitter }
72
+ : {})
73
+ }
31
74
 
32
- const siteConfigPath = findFile([
33
- "src/config/site.config.ts",
34
- "src/config/site.config.js",
35
- "src/config/site.config.mjs"
36
- ])
75
+ const seoLocales = options.locales
76
+ const privatePathPrefixes = options.privatePathPrefixes || []
37
77
 
38
78
  const dbPath = findFile(["src/db/index.ts", "src/db/index.js", "src/db/index.mjs"])
39
79
 
@@ -90,6 +130,10 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
90
130
  pattern,
91
131
  entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
92
132
  })
133
+ injectRoute({
134
+ pattern: "/sitemap-index.xml",
135
+ entrypoint: "@rimelight/seo/routes/sitemap-index.xml.ts"
136
+ })
93
137
  }
94
138
 
95
139
  if (options.rss !== false) {
@@ -163,6 +207,17 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
163
207
  })
164
208
  }
165
209
 
210
+ if (options.og !== false) {
211
+ const pattern =
212
+ typeof options.og === "object" && options.og.path
213
+ ? options.og.path
214
+ : "/open-graph/[...route].png"
215
+ injectRoute({
216
+ pattern,
217
+ entrypoint: "@rimelight/seo/routes/open-graph.[...route].png.ts"
218
+ })
219
+ }
220
+
166
221
  updateConfig({
167
222
  vite: {
168
223
  plugins: [
@@ -288,6 +343,9 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
288
343
  ) {
289
344
  return "\0virtual:rimelight-seo/options"
290
345
  }
346
+ if (id === "virtual:rimelight/seo") {
347
+ return "\0virtual:rimelight/seo"
348
+ }
291
349
  if (id === "virtual:rimelight-seo/config") {
292
350
  return "\0virtual:rimelight-seo/config"
293
351
  }
@@ -306,25 +364,26 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
306
364
  if (id === "\0virtual:rimelight-seo/options") {
307
365
  return `export default ${JSON.stringify(options)};`
308
366
  }
367
+ if (id === "\0virtual:rimelight/seo") {
368
+ return `export const siteConfig = ${JSON.stringify(normalizedSiteConfig)};
369
+ export const SEO_LOCALES = ${JSON.stringify(seoLocales)};
370
+ export const PRIVATE_PATH_PREFIXES = ${JSON.stringify(privatePathPrefixes)};
371
+ export const seoConfig = {
372
+ SEO_LOCALES: ${JSON.stringify(seoLocales)},
373
+ PRIVATE_PATH_PREFIXES: ${JSON.stringify(privatePathPrefixes)}
374
+ };
375
+ export default siteConfig;`
376
+ }
309
377
  if (id === "\0virtual:rimelight-seo/config") {
310
- if (seoConfigPath) {
311
- return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
312
- export const SEO_LOCALES = customConfig.SEO_LOCALES;
313
- export const PRIVATE_PATH_PREFIXES = customConfig.PRIVATE_PATH_PREFIXES || [];
314
- export const seoEntries = customConfig.seoEntries;
315
- export const rssEntries = customConfig.rssEntries;
316
- export default customConfig;`
317
- }
318
- return `export const SEO_LOCALES = undefined;
319
- export const PRIVATE_PATH_PREFIXES = [];
378
+ return `export const SEO_LOCALES = ${JSON.stringify(seoLocales)};
379
+ export const PRIVATE_PATH_PREFIXES = ${JSON.stringify(privatePathPrefixes)};
320
380
  export const seoEntries = async () => [];
321
- export const rssEntries = undefined;`
381
+ export const rssEntries = undefined;
382
+ export default { SEO_LOCALES: ${JSON.stringify(seoLocales)}, PRIVATE_PATH_PREFIXES: ${JSON.stringify(privatePathPrefixes)} };`
322
383
  }
323
384
  if (id === "\0virtual:rimelight-seo/site-config") {
324
- if (siteConfigPath) {
325
- return `export * from ${JSON.stringify(siteConfigPath)};`
326
- }
327
- return `export const siteConfig = { name: "Site", description: "", url: "" };`
385
+ return `export const siteConfig = ${JSON.stringify(normalizedSiteConfig)};
386
+ export default siteConfig;`
328
387
  }
329
388
  if (id === "\0virtual:rimelight-seo/db") {
330
389
  if (dbPath) {
@@ -0,0 +1,81 @@
1
+ import type { APIRoute } from "astro"
2
+ import { renderDefaultOg } from "../og.js"
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config"
4
+ import * as dbMod from "virtual:rimelight-seo/db"
5
+ import seoOptions from "virtual:rimelight-seo/options"
6
+
7
+ export const prerender = false
8
+
9
+ function getLocalizedText(val: unknown, locale = "en"): string {
10
+ if (typeof val === "object" && val !== null) {
11
+ const record = val as Record<string, string>
12
+ return record[locale] || Object.values(record)[0] || ""
13
+ }
14
+ if (typeof val === "string") return val
15
+ if (typeof val === "number" || typeof val === "boolean") return String(val)
16
+ return ""
17
+ }
18
+
19
+ export const GET: APIRoute = async ({ request }) => {
20
+ const url = new URL(request.url)
21
+ const routeParam = url.pathname.replace(/^\/open-graph\//, "").replace(/\.png$/, "")
22
+
23
+ const siteConfig = siteConfigMod.siteConfig || {}
24
+ const ogOpts = typeof seoOptions?.og === "object" ? seoOptions.og : {}
25
+
26
+ // Brand: explicit og.brand > seo.name > site.name > hostname
27
+ const brand = ogOpts.brand || (seoOptions as any)?.name || siteConfig.name || url.hostname
28
+
29
+ const defaultBg = ogOpts.defaultBackground ?? "dark"
30
+ const docsBg = ogOpts.docsBackground ?? "gradient"
31
+
32
+ let title = url.searchParams.get("title") || ""
33
+ let description = url.searchParams.get("description") || ""
34
+ let date = url.searchParams.get("pubDate") || ""
35
+ const badge =
36
+ url.searchParams.get("type") ||
37
+ (routeParam.includes("blog") ? "Blog Post" : routeParam.includes("legal") ? "Legal" : "")
38
+
39
+ // DB slug lookup -- same pattern as llms.txt.ts, gracefully skipped when db is absent
40
+ if (!title && routeParam && routeParam !== "page" && routeParam !== "default" && dbMod.db) {
41
+ try {
42
+ const { pages: pagesTable } = await import("#db/schema").catch(() => ({ pages: null }))
43
+ const { eq, and, isNull } = await import("drizzle-orm")
44
+
45
+ if (pagesTable) {
46
+ const slug = routeParam.split("/").pop() ?? ""
47
+ const page = await dbMod.db
48
+ .select({
49
+ title: pagesTable.title,
50
+ description: pagesTable.description,
51
+ postedAt: pagesTable.postedAt
52
+ })
53
+ .from(pagesTable)
54
+ .where(and(eq(pagesTable.slug, slug), isNull(pagesTable.deletedAt)))
55
+ .limit(1)
56
+ .then((r: any[]) => r[0])
57
+ .catch(() => undefined)
58
+
59
+ if (page) {
60
+ title = getLocalizedText(page.title)
61
+ description = description || getLocalizedText(page.description)
62
+ if (page.postedAt && !date) date = new Date(page.postedAt).toLocaleDateString()
63
+ }
64
+ }
65
+ } catch {
66
+ // DB unavailable -- fall through to query-param values
67
+ }
68
+ }
69
+
70
+ const isDocs = url.searchParams.get("isDocs") === "true" || routeParam.includes("docs")
71
+
72
+ return renderDefaultOg({
73
+ title: title || brand,
74
+ description,
75
+ brand,
76
+ badge,
77
+ date,
78
+ background: isDocs ? docsBg : defaultBg,
79
+ preview: url.searchParams.get("preview") === "1"
80
+ })
81
+ }
@@ -0,0 +1,20 @@
1
+ import type { APIRoute } from "astro"
2
+ import { buildSitemapIndexXml } from "../sitemap.js"
3
+ import * as siteConfigMod from "virtual:rimelight-seo/site-config"
4
+ import seoOptions from "virtual:rimelight-seo/options"
5
+
6
+ export const GET: APIRoute = ({ site, url }) => {
7
+ const siteUrl = site ? site.toString() : siteConfigMod.siteConfig?.url || url.origin
8
+
9
+ const sitemapOpts = typeof seoOptions?.sitemap === "object" ? seoOptions.sitemap : {}
10
+ const sitemapPattern = typeof sitemapOpts.path === "string" ? sitemapOpts.path : "/sitemap.xml"
11
+
12
+ const xml = buildSitemapIndexXml([new URL(sitemapPattern, siteUrl).toString()])
13
+
14
+ return new Response(xml, {
15
+ headers: {
16
+ "Content-Type": "application/xml; charset=utf-8",
17
+ "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400"
18
+ }
19
+ })
20
+ }
package/src/types.ts CHANGED
@@ -519,6 +519,47 @@ export interface AssetRouteOptions {
519
519
  }
520
520
 
521
521
  export interface RimelightSeoOptions {
522
+ /**
523
+ * Site identifier / domain (e.g. "rimelight.com")
524
+ */
525
+ id?: string
526
+ /**
527
+ * Primary site name (e.g. "Rimelight")
528
+ */
529
+ name?: string
530
+ /**
531
+ * Default site description
532
+ */
533
+ description?: string
534
+ /**
535
+ * Canonical origin URL (e.g. "https://rimelight.com")
536
+ */
537
+ url?: string
538
+ ogImage?: string
539
+ author?: string
540
+ email?: string
541
+ branding?: Partial<SiteConfig["branding"]>
542
+ titleTemplate?: string
543
+ maxDescriptionLength?: number
544
+ authorHandle?: string
545
+ openGraph?: OpenGraphMeta
546
+ twitter?: TwitterMeta
547
+ /**
548
+ * Full or partial SiteConfig object
549
+ */
550
+ site?: Partial<SiteConfig>
551
+ /**
552
+ * Locale mappings for hreflang / sitemaps
553
+ */
554
+ locales?: LocaleConfig | Record<string, string>
555
+ /**
556
+ * Path prefixes to exclude from sitemap and indexing
557
+ */
558
+ privatePathPrefixes?: string[]
559
+ /**
560
+ * Custom SEO entry generator function
561
+ */
562
+ entries?: () => Promise<SeoEntry[]> | SeoEntry[]
522
563
  /**
523
564
  * Sitemap configuration or false to disable automatic /sitemap.xml (default: true)
524
565
  */
@@ -558,6 +599,41 @@ export interface RimelightSeoOptions {
558
599
  * icons) from a single master icon (default: true)
559
600
  */
560
601
  assets?: boolean | AssetRouteOptions
602
+ /**
603
+ * Open Graph image route configuration, or false to disable automatic /open-graph/[...route].png
604
+ * (default: true). When enabled, the route reads brand/background from seo.name and injects a
605
+ * handler that supports query-param overrides and optional DB slug lookup.
606
+ */
607
+ og?: boolean | OgRouteOptions
608
+ }
609
+
610
+ /**
611
+ * Configuration for the auto-injected /open-graph/[...route].png route.
612
+ */
613
+ export interface OgRouteOptions {
614
+ /**
615
+ * Route pattern for the OG image handler.
616
+ *
617
+ * @default "/open-graph/[...route].png"
618
+ */
619
+ path?: string
620
+ /**
621
+ * Brand name shown top-left of every OG image. Defaults to seo.name / site.name from integration
622
+ * config.
623
+ */
624
+ brand?: string
625
+ /**
626
+ * Background style used for standard pages.
627
+ *
628
+ * @default "dark"
629
+ */
630
+ defaultBackground?: "dark" | "gradient" | (string & {})
631
+ /**
632
+ * Background style used when ?isDocs=true or the route segment contains "docs".
633
+ *
634
+ * @default "gradient"
635
+ */
636
+ docsBackground?: "dark" | "gradient" | (string & {})
561
637
  }
562
638
 
563
639
  export type { LlmsPage, LlmsTxtOptions, LlmsFullTxtOptions } from "./llms.js"