@rimelight/seo 0.0.8 → 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/LICENSE +21 -0
- package/dist/index.d.mts +2 -2
- package/dist/integration.mjs +57 -37
- package/dist/routes/open-graph._...route_.png.d.mts +5 -0
- package/dist/routes/open-graph._...route_.png.mjs +57 -0
- package/dist/routes/sitemap-index.xml.d.mts +4 -0
- package/dist/routes/sitemap-index.xml.mjs +16 -0
- package/dist/types.d.mts +75 -0
- package/package.json +9 -11
- package/src/env.d.ts +66 -55
- package/src/index.ts +1 -0
- package/src/integration.ts +83 -36
- package/src/routes/open-graph.[...route].png.ts +81 -0
- package/src/routes/sitemap-index.xml.ts +20 -0
- package/src/types.ts +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rimelight Entertainment
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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 };
|
package/dist/integration.mjs
CHANGED
|
@@ -15,21 +15,35 @@ function rimelightSeo(options = {}) {
|
|
|
15
15
|
}
|
|
16
16
|
return null;
|
|
17
17
|
};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 } : {}
|
|
27
44
|
};
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
"src/config/site.config.js",
|
|
31
|
-
"src/config/site.config.mjs"
|
|
32
|
-
]);
|
|
45
|
+
const seoLocales = options.locales;
|
|
46
|
+
const privatePathPrefixes = options.privatePathPrefixes || [];
|
|
33
47
|
const dbPath = findFile([
|
|
34
48
|
"src/db/index.ts",
|
|
35
49
|
"src/db/index.js",
|
|
@@ -63,10 +77,16 @@ function rimelightSeo(options = {}) {
|
|
|
63
77
|
pattern: typeof options.robots === "object" && options.robots.path ? options.robots.path : "/robots.txt",
|
|
64
78
|
entrypoint: "@rimelight/seo/routes/robots.txt.ts"
|
|
65
79
|
});
|
|
66
|
-
if (options.sitemap !== false)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
90
|
if (options.rss !== false) injectRoute({
|
|
71
91
|
pattern: typeof options.rss === "object" && options.rss.path ? options.rss.path : "/rss.xml",
|
|
72
92
|
entrypoint: "@rimelight/seo/routes/rss.xml.ts"
|
|
@@ -98,6 +118,10 @@ function rimelightSeo(options = {}) {
|
|
|
98
118
|
pattern: typeof options.manifest === "object" && options.manifest.path ? options.manifest.path : "/site.webmanifest",
|
|
99
119
|
entrypoint: "@rimelight/seo/routes/site.webmanifest.ts"
|
|
100
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
|
+
});
|
|
101
125
|
updateConfig({ vite: { plugins: [{
|
|
102
126
|
name: "vite-plugin-rimelight-seo-assets",
|
|
103
127
|
configureServer(server) {
|
|
@@ -245,6 +269,7 @@ function rimelightSeo(options = {}) {
|
|
|
245
269
|
name: "vite-plugin-rimelight-seo",
|
|
246
270
|
resolveId(id) {
|
|
247
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";
|
|
248
273
|
if (id === "virtual:rimelight-seo/config") return "\0virtual:rimelight-seo/config";
|
|
249
274
|
if (id === "virtual:rimelight-seo/site-config") return "\0virtual:rimelight-seo/site-config";
|
|
250
275
|
if (id === "virtual:rimelight-seo/db") return "\0virtual:rimelight-seo/db";
|
|
@@ -253,26 +278,21 @@ function rimelightSeo(options = {}) {
|
|
|
253
278
|
},
|
|
254
279
|
load(id) {
|
|
255
280
|
if (id === "\0virtual:rimelight-seo/options") return `export default ${JSON.stringify(options)};`;
|
|
256
|
-
if (id === "\0virtual:rimelight
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
export
|
|
264
|
-
export const
|
|
265
|
-
export
|
|
266
|
-
}
|
|
267
|
-
return `export const SEO_LOCALES = undefined;
|
|
268
|
-
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)};
|
|
269
291
|
export const seoEntries = async () => [];
|
|
270
|
-
export const rssEntries = undefined
|
|
271
|
-
|
|
272
|
-
if (id === "\0virtual:rimelight-seo/site-config") {
|
|
273
|
-
|
|
274
|
-
return `export const siteConfig = { name: "Site", description: "", url: "" };`;
|
|
275
|
-
}
|
|
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;`;
|
|
276
296
|
if (id === "\0virtual:rimelight-seo/db") {
|
|
277
297
|
if (dbPath) return `export * from ${JSON.stringify(dbPath)};`;
|
|
278
298
|
return `export const db = null;
|
|
@@ -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,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.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's SEO Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -72,14 +72,9 @@
|
|
|
72
72
|
"publishConfig": {
|
|
73
73
|
"access": "public"
|
|
74
74
|
},
|
|
75
|
-
"scripts": {
|
|
76
|
-
"build": "vp pack",
|
|
77
|
-
"prepack": "pnpm run build",
|
|
78
|
-
"check": "vp check --fix && astro check"
|
|
79
|
-
},
|
|
80
75
|
"devDependencies": {
|
|
81
|
-
"@rimelight/config": "
|
|
82
|
-
"astro": "7.3.
|
|
76
|
+
"@rimelight/config": "0.0.12",
|
|
77
|
+
"astro": "7.3.3",
|
|
83
78
|
"sharp": "^0.35.4",
|
|
84
79
|
"typescript": "6.0.3"
|
|
85
80
|
},
|
|
@@ -97,7 +92,10 @@
|
|
|
97
92
|
}
|
|
98
93
|
},
|
|
99
94
|
"engines": {
|
|
100
|
-
"node": ">=26.
|
|
95
|
+
"node": ">=26.9.0"
|
|
101
96
|
},
|
|
102
|
-
"
|
|
103
|
-
|
|
97
|
+
"scripts": {
|
|
98
|
+
"build": "vp pack",
|
|
99
|
+
"check": "vp check --fix && astro check"
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/env.d.ts
CHANGED
|
@@ -1,55 +1,66 @@
|
|
|
1
|
-
declare module "virtual:rimelight
|
|
2
|
-
const
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
declare module "virtual:rimelight-seo/
|
|
23
|
-
export const
|
|
24
|
-
export const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
export const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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"
|
package/src/integration.ts
CHANGED
|
@@ -23,27 +23,57 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
|
|
|
23
23
|
return null
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
: {})
|
|
40
73
|
}
|
|
41
74
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
"src/config/site.config.js",
|
|
45
|
-
"src/config/site.config.mjs"
|
|
46
|
-
])
|
|
75
|
+
const seoLocales = options.locales
|
|
76
|
+
const privatePathPrefixes = options.privatePathPrefixes || []
|
|
47
77
|
|
|
48
78
|
const dbPath = findFile(["src/db/index.ts", "src/db/index.js", "src/db/index.mjs"])
|
|
49
79
|
|
|
@@ -100,6 +130,10 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
|
|
|
100
130
|
pattern,
|
|
101
131
|
entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
|
|
102
132
|
})
|
|
133
|
+
injectRoute({
|
|
134
|
+
pattern: "/sitemap-index.xml",
|
|
135
|
+
entrypoint: "@rimelight/seo/routes/sitemap-index.xml.ts"
|
|
136
|
+
})
|
|
103
137
|
}
|
|
104
138
|
|
|
105
139
|
if (options.rss !== false) {
|
|
@@ -173,6 +207,17 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
|
|
|
173
207
|
})
|
|
174
208
|
}
|
|
175
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
|
+
|
|
176
221
|
updateConfig({
|
|
177
222
|
vite: {
|
|
178
223
|
plugins: [
|
|
@@ -298,6 +343,9 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
|
|
|
298
343
|
) {
|
|
299
344
|
return "\0virtual:rimelight-seo/options"
|
|
300
345
|
}
|
|
346
|
+
if (id === "virtual:rimelight/seo") {
|
|
347
|
+
return "\0virtual:rimelight/seo"
|
|
348
|
+
}
|
|
301
349
|
if (id === "virtual:rimelight-seo/config") {
|
|
302
350
|
return "\0virtual:rimelight-seo/config"
|
|
303
351
|
}
|
|
@@ -316,27 +364,26 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
|
|
|
316
364
|
if (id === "\0virtual:rimelight-seo/options") {
|
|
317
365
|
return `export default ${JSON.stringify(options)};`
|
|
318
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
|
+
}
|
|
319
377
|
if (id === "\0virtual:rimelight-seo/config") {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const rssEntriesExport = hasConfigExport("rssEntries")
|
|
323
|
-
return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
|
|
324
|
-
export const SEO_LOCALES = customConfig.SEO_LOCALES;
|
|
325
|
-
export const PRIVATE_PATH_PREFIXES = customConfig.PRIVATE_PATH_PREFIXES || [];
|
|
326
|
-
export const seoEntries = ${seoEntriesExport ? "customConfig.seoEntries" : "undefined"};
|
|
327
|
-
export const rssEntries = ${rssEntriesExport ? "customConfig.rssEntries" : "undefined"};
|
|
328
|
-
export default customConfig;`
|
|
329
|
-
}
|
|
330
|
-
return `export const SEO_LOCALES = undefined;
|
|
331
|
-
export const PRIVATE_PATH_PREFIXES = [];
|
|
378
|
+
return `export const SEO_LOCALES = ${JSON.stringify(seoLocales)};
|
|
379
|
+
export const PRIVATE_PATH_PREFIXES = ${JSON.stringify(privatePathPrefixes)};
|
|
332
380
|
export const seoEntries = async () => [];
|
|
333
|
-
export const rssEntries = undefined
|
|
381
|
+
export const rssEntries = undefined;
|
|
382
|
+
export default { SEO_LOCALES: ${JSON.stringify(seoLocales)}, PRIVATE_PATH_PREFIXES: ${JSON.stringify(privatePathPrefixes)} };`
|
|
334
383
|
}
|
|
335
384
|
if (id === "\0virtual:rimelight-seo/site-config") {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
return `export const siteConfig = { name: "Site", description: "", url: "" };`
|
|
385
|
+
return `export const siteConfig = ${JSON.stringify(normalizedSiteConfig)};
|
|
386
|
+
export default siteConfig;`
|
|
340
387
|
}
|
|
341
388
|
if (id === "\0virtual:rimelight-seo/db") {
|
|
342
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"
|