@rimelight/seo 0.0.6 → 0.0.8
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/assets.d.mts +39 -0
- package/dist/assets.mjs +96 -0
- package/dist/catalog.d.mts +7 -0
- package/dist/catalog.mjs +10 -0
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +5 -1
- package/dist/integration.mjs +194 -3
- package/dist/manifest.d.mts +7 -0
- package/dist/manifest.mjs +44 -0
- package/dist/og.mjs +1 -1
- package/dist/routes/api-catalog.d.mts +5 -0
- package/dist/routes/api-catalog.mjs +38 -0
- package/dist/routes/apple-touch-icon.png.d.mts +5 -0
- package/dist/routes/apple-touch-icon.png.mjs +17 -0
- package/dist/routes/favicon.ico.d.mts +5 -0
- package/dist/routes/favicon.ico.mjs +14 -0
- package/dist/routes/favicon.svg.d.mts +5 -0
- package/dist/routes/favicon.svg.mjs +12 -0
- package/dist/routes/icon-192.png.d.mts +5 -0
- package/dist/routes/icon-192.png.mjs +17 -0
- package/dist/routes/icon-512.png.d.mts +5 -0
- package/dist/routes/icon-512.png.mjs +17 -0
- package/dist/routes/icon-maskable-512.png.d.mts +5 -0
- package/dist/routes/icon-maskable-512.png.mjs +18 -0
- package/dist/routes/llms.txt.mjs +1 -1
- package/dist/routes/robots.txt.mjs +1 -1
- package/dist/routes/rss.xml.mjs +48 -20
- package/dist/routes/security.txt.d.mts +5 -0
- package/dist/routes/security.txt.mjs +24 -0
- package/dist/routes/site.webmanifest.d.mts +5 -0
- package/dist/routes/site.webmanifest.mjs +23 -0
- package/dist/routes/sitemap.xml.mjs +1 -1
- package/dist/security.d.mts +7 -0
- package/dist/security.mjs +34 -0
- package/dist/types.d.mts +191 -8
- package/package.json +30 -7
- package/src/assets.test.ts +70 -0
- package/src/assets.ts +137 -0
- package/src/catalog.test.ts +26 -0
- package/src/catalog.ts +11 -0
- package/src/components/SEOHead.astro +1 -1
- package/src/env.d.ts +55 -33
- package/src/index.ts +18 -1
- package/src/integration.ts +365 -172
- package/src/manifest.test.ts +41 -0
- package/src/manifest.ts +59 -0
- package/src/og.ts +1 -9
- package/src/routes/api-catalog.ts +55 -0
- package/src/routes/apple-touch-icon.png.ts +19 -0
- package/src/routes/favicon.ico.ts +19 -0
- package/src/routes/favicon.svg.ts +17 -0
- package/src/routes/icon-192.png.ts +19 -0
- package/src/routes/icon-512.png.ts +19 -0
- package/src/routes/icon-maskable-512.png.ts +24 -0
- package/src/routes/llms-full.txt.ts +77 -77
- package/src/routes/llms.txt.ts +93 -90
- package/src/routes/robots.txt.ts +63 -63
- package/src/routes/rss.xml.ts +129 -87
- package/src/routes/security.txt.ts +30 -0
- package/src/routes/site.webmanifest.ts +28 -0
- package/src/routes/sitemap.xml.ts +43 -43
- package/src/rss.test.ts +28 -28
- package/src/rss.ts +63 -63
- package/src/security.test.ts +46 -0
- package/src/security.ts +78 -0
- package/src/types.ts +564 -367
- package/LICENSE +0 -21
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { generateMaskablePng } from "../assets.mjs";
|
|
2
|
+
import { backgroundColor, masterIconBuffer } from "virtual:rimelight-seo/assets";
|
|
3
|
+
//#region src/routes/icon-maskable-512.png.ts
|
|
4
|
+
const prerender = true;
|
|
5
|
+
const GET = async () => {
|
|
6
|
+
if (!masterIconBuffer) return new Response(null, { status: 404 });
|
|
7
|
+
const png = await generateMaskablePng(masterIconBuffer, {
|
|
8
|
+
size: 512,
|
|
9
|
+
background: backgroundColor || "#ffffff",
|
|
10
|
+
paddingRatio: .15
|
|
11
|
+
});
|
|
12
|
+
return new Response(new Uint8Array(png), { headers: {
|
|
13
|
+
"Content-Type": "image/png",
|
|
14
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
15
|
+
} });
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { GET, prerender };
|
package/dist/routes/llms.txt.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildLlmsTxt } from "../llms.mjs";
|
|
2
|
+
import seoOptions from "virtual:rimelight-seo/options";
|
|
2
3
|
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
3
4
|
import * as dbMod from "virtual:rimelight-seo/db";
|
|
4
5
|
import * as corpusMod from "virtual:rimelight-seo/corpus";
|
|
5
|
-
import seoOptions from "virtual:rimelight-seo/options";
|
|
6
6
|
//#region src/routes/llms.txt.ts
|
|
7
7
|
const prerender = false;
|
|
8
8
|
const GET = async ({ site, url }) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildRobotsTxt } from "../robots.mjs";
|
|
2
|
-
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
3
2
|
import seoOptions from "virtual:rimelight-seo/options";
|
|
3
|
+
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
4
4
|
import * as seoConfig from "virtual:rimelight-seo/config";
|
|
5
5
|
//#region src/routes/robots.txt.ts
|
|
6
6
|
const GET = ({ site, url }) => {
|
package/dist/routes/rss.xml.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildRssXml } from "../rss.mjs";
|
|
2
|
+
import seoOptions from "virtual:rimelight-seo/options";
|
|
2
3
|
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
3
4
|
import * as dbMod from "virtual:rimelight-seo/db";
|
|
4
|
-
import seoOptions from "virtual:rimelight-seo/options";
|
|
5
5
|
import * as seoConfig from "virtual:rimelight-seo/config";
|
|
6
6
|
//#region src/routes/rss.xml.ts
|
|
7
7
|
const GET = async ({ site, url }) => {
|
|
@@ -10,29 +10,57 @@ const GET = async ({ site, url }) => {
|
|
|
10
10
|
const siteUrl = site ? site.toString() : rssOpts.site || siteConfig.url || url.origin;
|
|
11
11
|
let items = [];
|
|
12
12
|
if (Array.isArray(rssOpts.items)) items = rssOpts.items;
|
|
13
|
+
else if (typeof rssOpts.items === "function") items = await rssOpts.items();
|
|
13
14
|
else if (typeof seoConfig.rssEntries === "function") items = await seoConfig.rssEntries();
|
|
14
|
-
else
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
else {
|
|
16
|
+
const normalizedCollections = (rssOpts.collections && rssOpts.collections.length > 0 ? rssOpts.collections : [
|
|
17
|
+
"blog",
|
|
18
|
+
"posts",
|
|
19
|
+
"news",
|
|
20
|
+
"articles"
|
|
21
|
+
]).map((c) => {
|
|
22
|
+
if (typeof c === "string") return {
|
|
23
|
+
name: c,
|
|
24
|
+
pathPrefix: `/${c.replace(/^\/+|\/+$/g, "")}`
|
|
25
|
+
};
|
|
18
26
|
return {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
link: `/blog/${p.slug}`,
|
|
22
|
-
pubDate: p.postedAt || p.createdAt || /* @__PURE__ */ new Date()
|
|
27
|
+
name: c.name,
|
|
28
|
+
pathPrefix: c.pathPrefix ? `/${c.pathPrefix.replace(/^\/+|\/+$/g, "")}` : `/${c.name.replace(/^\/+|\/+$/g, "")}`
|
|
23
29
|
};
|
|
24
30
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
if (dbMod.db) try {
|
|
32
|
+
const { pages } = await import("#db/schema").catch(() => ({ pages: null }));
|
|
33
|
+
const { and, isNull } = await import("drizzle-orm");
|
|
34
|
+
if (pages) {
|
|
35
|
+
const collectionNames = normalizedCollections.map((c) => c.name);
|
|
36
|
+
const prefixMap = new Map(normalizedCollections.map((c) => [c.name, c.pathPrefix]));
|
|
37
|
+
const matchingPages = (await dbMod.db.select().from(pages).where(and(isNull(pages.deletedAt))).catch(() => [])).filter((p) => collectionNames.includes(p.type));
|
|
38
|
+
for (const p of matchingPages) {
|
|
39
|
+
const prefix = prefixMap.get(p.type) || `/${p.type}`;
|
|
40
|
+
const title = typeof p.title === "string" ? p.title : p.title?.en || p.title && Object.values(p.title)[0] || p.slug;
|
|
41
|
+
const description = typeof p.description === "string" ? p.description : p.description?.en || p.description && Object.values(p.description)[0] || "";
|
|
42
|
+
items.push({
|
|
43
|
+
title,
|
|
44
|
+
description,
|
|
45
|
+
link: `${prefix}/${p.slug}`,
|
|
46
|
+
pubDate: p.postedAt || p.createdAt || /* @__PURE__ */ new Date()
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
if (items.length === 0) try {
|
|
52
|
+
const { getCollection } = await import("astro:content");
|
|
53
|
+
for (const coll of normalizedCollections) try {
|
|
54
|
+
const collEntries = await getCollection(coll.name);
|
|
55
|
+
if (Array.isArray(collEntries)) for (const post of collEntries) items.push({
|
|
56
|
+
title: post.data?.title || post.id,
|
|
57
|
+
pubDate: post.data?.pubDate || post.data?.date || /* @__PURE__ */ new Date(),
|
|
58
|
+
description: post.data?.description || post.data?.summary,
|
|
59
|
+
link: `${coll.pathPrefix}/${post.id}/`
|
|
60
|
+
});
|
|
61
|
+
} catch {}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
36
64
|
const feedTitle = rssOpts.title || siteConfig.name || "RSS Feed";
|
|
37
65
|
const feedDesc = rssOpts.description || siteConfig.description || "";
|
|
38
66
|
const xml = buildRssXml({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { buildSecurityTxt } from "../security.mjs";
|
|
2
|
+
import seoOptions from "virtual:rimelight-seo/options";
|
|
3
|
+
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
4
|
+
//#region src/routes/security.txt.ts
|
|
5
|
+
const prerender = false;
|
|
6
|
+
const GET = ({ site, url }) => {
|
|
7
|
+
const siteConfig = siteConfigMod.siteConfig || {};
|
|
8
|
+
const siteUrl = site ? site.toString() : siteConfig.url || url.origin;
|
|
9
|
+
const secOpts = typeof seoOptions?.security === "object" ? seoOptions.security : {};
|
|
10
|
+
const defaultContact = siteConfig.email ? `mailto:${siteConfig.email}` : void 0;
|
|
11
|
+
const contacts = secOpts.contact || defaultContact || `mailto:security@${new URL(siteUrl).hostname}`;
|
|
12
|
+
const canonical = secOpts.canonical || new URL("/.well-known/security.txt", siteUrl).toString();
|
|
13
|
+
const content = buildSecurityTxt({
|
|
14
|
+
contact: contacts,
|
|
15
|
+
canonical,
|
|
16
|
+
...secOpts
|
|
17
|
+
});
|
|
18
|
+
return new Response(content, { headers: {
|
|
19
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
20
|
+
"Cache-Control": "public, max-age=86400, stale-while-revalidate=604800"
|
|
21
|
+
} });
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { GET, prerender };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { buildWebManifest } from "../manifest.mjs";
|
|
2
|
+
import seoOptions from "virtual:rimelight-seo/options";
|
|
3
|
+
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
4
|
+
//#region src/routes/site.webmanifest.ts
|
|
5
|
+
const prerender = false;
|
|
6
|
+
const GET = () => {
|
|
7
|
+
const siteConfig = siteConfigMod.siteConfig || {};
|
|
8
|
+
const manifestOpts = typeof seoOptions?.manifest === "object" ? seoOptions.manifest : {};
|
|
9
|
+
const manifest = buildWebManifest({
|
|
10
|
+
name: manifestOpts.name || siteConfig.name || "Site",
|
|
11
|
+
shortName: manifestOpts.shortName || siteConfig.name || "Site",
|
|
12
|
+
description: manifestOpts.description || siteConfig.description || "",
|
|
13
|
+
backgroundColor: manifestOpts.backgroundColor || siteConfig.branding?.colors?.backgroundColor || "#ffffff",
|
|
14
|
+
themeColor: manifestOpts.themeColor || siteConfig.branding?.colors?.themeColor || "#ffffff",
|
|
15
|
+
...manifestOpts
|
|
16
|
+
});
|
|
17
|
+
return new Response(JSON.stringify(manifest, null, 2), { headers: {
|
|
18
|
+
"Content-Type": "application/manifest+json; charset=utf-8",
|
|
19
|
+
"Cache-Control": "public, max-age=86400, stale-while-revalidate=604800"
|
|
20
|
+
} });
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { GET, prerender };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildSitemapUrls, buildSitemapXml } from "../sitemap.mjs";
|
|
2
|
-
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
3
2
|
import seoOptions from "virtual:rimelight-seo/options";
|
|
3
|
+
import * as siteConfigMod from "virtual:rimelight-seo/site-config";
|
|
4
4
|
import * as seoConfig from "virtual:rimelight-seo/config";
|
|
5
5
|
//#region src/routes/sitemap.xml.ts
|
|
6
6
|
const GET = async ({ site, url }) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SecurityTxtOptions } from "./types.mjs";
|
|
2
|
+
//#region src/security.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Build RFC 9116 compliant security.txt content. https://www.rfc-editor.org/rfc/rfc9116
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildSecurityTxt(options: SecurityTxtOptions): string;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/security.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build RFC 9116 compliant security.txt content. https://www.rfc-editor.org/rfc/rfc9116
|
|
4
|
+
*/
|
|
5
|
+
function buildSecurityTxt(options) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
const contacts = Array.isArray(options.contact) ? options.contact : options.contact ? [options.contact] : [];
|
|
8
|
+
for (const contact of contacts) lines.push(`Contact: ${contact}`);
|
|
9
|
+
let expiresStr;
|
|
10
|
+
if (options.expires instanceof Date) expiresStr = options.expires.toISOString();
|
|
11
|
+
else if (typeof options.expires === "string" && options.expires.trim().length > 0) expiresStr = options.expires;
|
|
12
|
+
else {
|
|
13
|
+
const oneYearFromNow = /* @__PURE__ */ new Date();
|
|
14
|
+
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
|
|
15
|
+
expiresStr = oneYearFromNow.toISOString();
|
|
16
|
+
}
|
|
17
|
+
lines.push(`Expires: ${expiresStr}`);
|
|
18
|
+
if (options.encryption) {
|
|
19
|
+
const encList = Array.isArray(options.encryption) ? options.encryption : [options.encryption];
|
|
20
|
+
for (const enc of encList) lines.push(`Encryption: ${enc}`);
|
|
21
|
+
}
|
|
22
|
+
if (options.acknowledgments) {
|
|
23
|
+
const acks = Array.isArray(options.acknowledgments) ? options.acknowledgments : [options.acknowledgments];
|
|
24
|
+
for (const ack of acks) lines.push(`Acknowledgments: ${ack}`);
|
|
25
|
+
}
|
|
26
|
+
if (options.preferredLanguages) lines.push(`Preferred-Languages: ${options.preferredLanguages}`);
|
|
27
|
+
if (options.canonical) lines.push(`Canonical: ${options.canonical}`);
|
|
28
|
+
if (options.policy) lines.push(`Policy: ${options.policy}`);
|
|
29
|
+
if (options.hiring) lines.push(`Hiring: ${options.hiring}`);
|
|
30
|
+
if (options.csaf) lines.push(`CSAF: ${options.csaf}`);
|
|
31
|
+
return lines.join("\n") + "\n";
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { buildSecurityTxt };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as BreadcrumbItem, t as ArticleSchemaOptions } from "./schema-C8v69blQ.mjs";
|
|
2
|
-
import { n as LlmsPage, r as LlmsTxtOptions
|
|
2
|
+
import { n as LlmsPage, r as LlmsTxtOptions, t as LlmsFullTxtOptions } from "./llms-BVLxzjr-.mjs";
|
|
3
3
|
//#region src/types.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Change frequency for sitemap entries
|
|
@@ -115,16 +115,17 @@ export interface SiteConfig {
|
|
|
115
115
|
name: string;
|
|
116
116
|
description: string;
|
|
117
117
|
url: string;
|
|
118
|
-
ogImage
|
|
118
|
+
ogImage?: string;
|
|
119
119
|
author: string;
|
|
120
120
|
email: string;
|
|
121
121
|
branding: {
|
|
122
122
|
logo: {
|
|
123
123
|
alt: string;
|
|
124
124
|
};
|
|
125
|
-
favicon
|
|
125
|
+
favicon?: {
|
|
126
126
|
svg: string;
|
|
127
127
|
};
|
|
128
|
+
icon?: string;
|
|
128
129
|
colors: {
|
|
129
130
|
themeColor: string;
|
|
130
131
|
backgroundColor: string;
|
|
@@ -132,7 +133,7 @@ export interface SiteConfig {
|
|
|
132
133
|
};
|
|
133
134
|
seo: {
|
|
134
135
|
titleTemplate: string;
|
|
135
|
-
ogImageFallback
|
|
136
|
+
ogImageFallback?: string;
|
|
136
137
|
maxDescriptionLength: number;
|
|
137
138
|
authorHandle?: string;
|
|
138
139
|
};
|
|
@@ -290,15 +291,23 @@ export interface RssFeedOptions {
|
|
|
290
291
|
language?: string;
|
|
291
292
|
items: RssItem[];
|
|
292
293
|
}
|
|
293
|
-
export interface
|
|
294
|
+
export interface RssCollectionConfig {
|
|
295
|
+
name: string;
|
|
296
|
+
pathPrefix?: string;
|
|
297
|
+
}
|
|
298
|
+
export interface RssOptions extends Omit<Partial<RssFeedOptions>, "items"> {
|
|
294
299
|
/**
|
|
295
300
|
* Custom RSS route pattern (default: "/rss.xml")
|
|
296
301
|
*/
|
|
297
302
|
path?: string;
|
|
298
303
|
/**
|
|
299
|
-
* Custom items loader
|
|
304
|
+
* Custom items loader or static items array
|
|
305
|
+
*/
|
|
306
|
+
items?: RssItem[] | (() => Promise<RssItem[]> | RssItem[]);
|
|
307
|
+
/**
|
|
308
|
+
* Collections to include in RSS feed (defaults to ["blog", "posts", "news", "articles"])
|
|
300
309
|
*/
|
|
301
|
-
|
|
310
|
+
collections?: (string | RssCollectionConfig)[];
|
|
302
311
|
}
|
|
303
312
|
export interface SitemapOptions {
|
|
304
313
|
/**
|
|
@@ -310,6 +319,125 @@ export interface SitemapOptions {
|
|
|
310
319
|
locales?: LocaleConfig;
|
|
311
320
|
[key: string]: unknown;
|
|
312
321
|
}
|
|
322
|
+
export interface SecurityTxtOptions {
|
|
323
|
+
/**
|
|
324
|
+
* Contact URI or list of contact URIs (e.g. "mailto:security@example.com") - REQUIRED by RFC 9116
|
|
325
|
+
*/
|
|
326
|
+
contact: string | string[];
|
|
327
|
+
/**
|
|
328
|
+
* Expiration date/time (ISO 8601 string or Date) - REQUIRED by RFC 9116
|
|
329
|
+
*/
|
|
330
|
+
expires?: Date | string;
|
|
331
|
+
/**
|
|
332
|
+
* URI to PGP/GPG public key
|
|
333
|
+
*/
|
|
334
|
+
encryption?: string | string[];
|
|
335
|
+
/**
|
|
336
|
+
* URI to security acknowledgments / hall of fame
|
|
337
|
+
*/
|
|
338
|
+
acknowledgments?: string | string[];
|
|
339
|
+
/**
|
|
340
|
+
* Comma-separated list of preferred languages (e.g. "en, pt")
|
|
341
|
+
*/
|
|
342
|
+
preferredLanguages?: string;
|
|
343
|
+
/**
|
|
344
|
+
* Canonical URI of the security.txt file
|
|
345
|
+
*/
|
|
346
|
+
canonical?: string;
|
|
347
|
+
/**
|
|
348
|
+
* URI to security policy
|
|
349
|
+
*/
|
|
350
|
+
policy?: string;
|
|
351
|
+
/**
|
|
352
|
+
* URI to security hiring page
|
|
353
|
+
*/
|
|
354
|
+
hiring?: string;
|
|
355
|
+
/**
|
|
356
|
+
* URI to Common Security Advisory Framework (CSAF) provider
|
|
357
|
+
*/
|
|
358
|
+
csaf?: string;
|
|
359
|
+
}
|
|
360
|
+
export interface SecurityRouteOptions extends Partial<SecurityTxtOptions> {
|
|
361
|
+
/**
|
|
362
|
+
* Route pattern (default: "/.well-known/security.txt")
|
|
363
|
+
*/
|
|
364
|
+
path?: string;
|
|
365
|
+
/**
|
|
366
|
+
* Whether to also inject legacy fallback route at "/security.txt" (default: true)
|
|
367
|
+
*/
|
|
368
|
+
legacyRoute?: boolean;
|
|
369
|
+
}
|
|
370
|
+
export interface WebManifestIcon {
|
|
371
|
+
src: string;
|
|
372
|
+
sizes: string;
|
|
373
|
+
type: string;
|
|
374
|
+
purpose?: "any" | "maskable" | "monochrome";
|
|
375
|
+
}
|
|
376
|
+
export interface WebManifestShortcut {
|
|
377
|
+
name: string;
|
|
378
|
+
url: string;
|
|
379
|
+
short_name?: string;
|
|
380
|
+
description?: string;
|
|
381
|
+
icons?: WebManifestIcon[];
|
|
382
|
+
}
|
|
383
|
+
export interface WebManifest {
|
|
384
|
+
name: string;
|
|
385
|
+
short_name?: string;
|
|
386
|
+
description?: string;
|
|
387
|
+
start_url: string;
|
|
388
|
+
display: "fullscreen" | "standalone" | "minimal-ui" | "browser";
|
|
389
|
+
background_color: string;
|
|
390
|
+
theme_color: string;
|
|
391
|
+
icons: WebManifestIcon[];
|
|
392
|
+
shortcuts?: WebManifestShortcut[];
|
|
393
|
+
categories?: string[];
|
|
394
|
+
lang?: string;
|
|
395
|
+
dir?: "auto" | "ltr" | "rtl";
|
|
396
|
+
orientation?: "any" | "natural" | "landscape" | "portrait";
|
|
397
|
+
scope?: string;
|
|
398
|
+
[key: string]: unknown;
|
|
399
|
+
}
|
|
400
|
+
export interface WebManifestOptions {
|
|
401
|
+
name: string;
|
|
402
|
+
shortName?: string;
|
|
403
|
+
description?: string;
|
|
404
|
+
startUrl?: string;
|
|
405
|
+
display?: "fullscreen" | "standalone" | "minimal-ui" | "browser";
|
|
406
|
+
backgroundColor?: string;
|
|
407
|
+
themeColor?: string;
|
|
408
|
+
icons?: WebManifestIcon[];
|
|
409
|
+
shortcuts?: WebManifestShortcut[];
|
|
410
|
+
categories?: string[];
|
|
411
|
+
lang?: string;
|
|
412
|
+
dir?: "auto" | "ltr" | "rtl";
|
|
413
|
+
orientation?: "any" | "natural" | "landscape" | "portrait";
|
|
414
|
+
scope?: string;
|
|
415
|
+
}
|
|
416
|
+
export interface ManifestRouteOptions extends Partial<WebManifestOptions> {
|
|
417
|
+
/**
|
|
418
|
+
* Route pattern (default: "/site.webmanifest")
|
|
419
|
+
*/
|
|
420
|
+
path?: string;
|
|
421
|
+
}
|
|
422
|
+
export interface ApiCatalogItem {
|
|
423
|
+
rel: string;
|
|
424
|
+
href: string;
|
|
425
|
+
type?: string;
|
|
426
|
+
title?: string;
|
|
427
|
+
[key: string]: unknown;
|
|
428
|
+
}
|
|
429
|
+
export interface ApiCatalog {
|
|
430
|
+
"api-catalog": ApiCatalogItem[];
|
|
431
|
+
}
|
|
432
|
+
export interface ApiCatalogOptions {
|
|
433
|
+
items: ApiCatalogItem[];
|
|
434
|
+
}
|
|
435
|
+
export interface ApiCatalogRouteOptions extends Partial<ApiCatalogOptions> {
|
|
436
|
+
/**
|
|
437
|
+
* Route pattern (default: "/.well-known/api-catalog")
|
|
438
|
+
*/
|
|
439
|
+
path?: string;
|
|
440
|
+
}
|
|
313
441
|
export interface RobotsRouteOptions extends Partial<RobotsOptions> {
|
|
314
442
|
/**
|
|
315
443
|
* Route pattern (default: "/robots.txt")
|
|
@@ -328,6 +456,42 @@ export interface LlmsFullRouteOptions extends Partial<LlmsFullTxtOptions> {
|
|
|
328
456
|
*/
|
|
329
457
|
path?: string;
|
|
330
458
|
}
|
|
459
|
+
export interface AssetRouteOptions {
|
|
460
|
+
/**
|
|
461
|
+
* Path to the master SVG/PNG icon file (relative to project root or absolute). If omitted, will
|
|
462
|
+
* search in src/assets/logos/ or siteConfig.branding.icon.
|
|
463
|
+
*/
|
|
464
|
+
icon?: string;
|
|
465
|
+
/**
|
|
466
|
+
* Background color for maskable icons (defaults to siteConfig.branding.colors.backgroundColor or
|
|
467
|
+
* #ffffff)
|
|
468
|
+
*/
|
|
469
|
+
backgroundColor?: string;
|
|
470
|
+
/**
|
|
471
|
+
* Whether to inject /favicon.ico (default: true)
|
|
472
|
+
*/
|
|
473
|
+
faviconIco?: boolean;
|
|
474
|
+
/**
|
|
475
|
+
* Whether to inject /apple-touch-icon.png (default: true)
|
|
476
|
+
*/
|
|
477
|
+
appleTouchIcon?: boolean;
|
|
478
|
+
/**
|
|
479
|
+
* Whether to inject /icon-192.png (default: true)
|
|
480
|
+
*/
|
|
481
|
+
icon192?: boolean;
|
|
482
|
+
/**
|
|
483
|
+
* Whether to inject /icon-512.png (default: true)
|
|
484
|
+
*/
|
|
485
|
+
icon512?: boolean;
|
|
486
|
+
/**
|
|
487
|
+
* Whether to inject /icon-maskable-512.png (default: true)
|
|
488
|
+
*/
|
|
489
|
+
iconMaskable?: boolean;
|
|
490
|
+
/**
|
|
491
|
+
* Whether to generate and emit /og.png branded card (default: true)
|
|
492
|
+
*/
|
|
493
|
+
ogImage?: boolean;
|
|
494
|
+
}
|
|
331
495
|
export interface RimelightSeoOptions {
|
|
332
496
|
/**
|
|
333
497
|
* Sitemap configuration or false to disable automatic /sitemap.xml (default: true)
|
|
@@ -349,6 +513,25 @@ export interface RimelightSeoOptions {
|
|
|
349
513
|
* LLMs-full.txt configuration or false to disable automatic /llms-full.txt (default: true)
|
|
350
514
|
*/
|
|
351
515
|
llmsFull?: boolean | LlmsFullRouteOptions;
|
|
516
|
+
/**
|
|
517
|
+
* Security.txt configuration or false to disable automatic /.well-known/security.txt (default:
|
|
518
|
+
* true)
|
|
519
|
+
*/
|
|
520
|
+
security?: boolean | SecurityRouteOptions;
|
|
521
|
+
/**
|
|
522
|
+
* RFC 9652 API Catalog configuration or false to disable automatic /.well-known/api-catalog
|
|
523
|
+
* (default: true)
|
|
524
|
+
*/
|
|
525
|
+
apiCatalog?: boolean | ApiCatalogRouteOptions;
|
|
526
|
+
/**
|
|
527
|
+
* Web App Manifest configuration or false to disable automatic /site.webmanifest (default: true)
|
|
528
|
+
*/
|
|
529
|
+
manifest?: boolean | ManifestRouteOptions;
|
|
530
|
+
/**
|
|
531
|
+
* Automatic generation of compliant public image assets (favicon.ico, apple-touch-icon.png, PWA
|
|
532
|
+
* icons) from a single master icon (default: true)
|
|
533
|
+
*/
|
|
534
|
+
assets?: boolean | AssetRouteOptions;
|
|
352
535
|
}
|
|
353
536
|
//#endregion
|
|
354
|
-
export type { ArticleSchemaOptions, BreadcrumbItem, LlmsFullTxtOptions
|
|
537
|
+
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.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's SEO Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -45,10 +45,26 @@
|
|
|
45
45
|
"types": "./dist/rss.d.mts",
|
|
46
46
|
"import": "./dist/rss.mjs"
|
|
47
47
|
},
|
|
48
|
+
"./security": {
|
|
49
|
+
"types": "./dist/security.d.mts",
|
|
50
|
+
"import": "./dist/security.mjs"
|
|
51
|
+
},
|
|
52
|
+
"./manifest": {
|
|
53
|
+
"types": "./dist/manifest.d.mts",
|
|
54
|
+
"import": "./dist/manifest.mjs"
|
|
55
|
+
},
|
|
56
|
+
"./catalog": {
|
|
57
|
+
"types": "./dist/catalog.d.mts",
|
|
58
|
+
"import": "./dist/catalog.mjs"
|
|
59
|
+
},
|
|
48
60
|
"./og": {
|
|
49
61
|
"types": "./dist/og.d.mts",
|
|
50
62
|
"import": "./dist/og.mjs"
|
|
51
63
|
},
|
|
64
|
+
"./assets": {
|
|
65
|
+
"types": "./dist/assets.d.mts",
|
|
66
|
+
"import": "./dist/assets.mjs"
|
|
67
|
+
},
|
|
52
68
|
"./components/*": "./src/components/*",
|
|
53
69
|
"./routes/*": "./src/routes/*",
|
|
54
70
|
"./*": "./src/*"
|
|
@@ -56,16 +72,26 @@
|
|
|
56
72
|
"publishConfig": {
|
|
57
73
|
"access": "public"
|
|
58
74
|
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "vp pack",
|
|
77
|
+
"prepack": "pnpm run build",
|
|
78
|
+
"check": "vp check --fix && astro check"
|
|
79
|
+
},
|
|
59
80
|
"devDependencies": {
|
|
60
|
-
"@rimelight/config": "
|
|
81
|
+
"@rimelight/config": "workspace:*",
|
|
61
82
|
"astro": "7.3.2",
|
|
83
|
+
"sharp": "^0.35.4",
|
|
62
84
|
"typescript": "6.0.3"
|
|
63
85
|
},
|
|
64
86
|
"peerDependencies": {
|
|
65
87
|
"astro": ">=7.0.0",
|
|
88
|
+
"sharp": ">=0.33.0",
|
|
66
89
|
"takumi-js": ">=2.0.0"
|
|
67
90
|
},
|
|
68
91
|
"peerDependenciesMeta": {
|
|
92
|
+
"sharp": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
69
95
|
"takumi-js": {
|
|
70
96
|
"optional": true
|
|
71
97
|
}
|
|
@@ -73,8 +99,5 @@
|
|
|
73
99
|
"engines": {
|
|
74
100
|
"node": ">=26.8.2"
|
|
75
101
|
},
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
"check": "vp check --fix && astro check"
|
|
79
|
-
}
|
|
80
|
-
}
|
|
102
|
+
"packageManager": "pnpm@12.4.1"
|
|
103
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from "vite-plus/test"
|
|
2
|
+
import { generatePng, generateMaskablePng, generateIco, generateOgCard } from "./assets.js"
|
|
3
|
+
import sharp from "sharp"
|
|
4
|
+
|
|
5
|
+
const sampleSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
|
6
|
+
<circle cx="50" cy="50" r="40" fill="#ff0000" />
|
|
7
|
+
</svg>`
|
|
8
|
+
|
|
9
|
+
describe("Asset Generation", () => {
|
|
10
|
+
it("generates PNG buffer with correct dimensions", async () => {
|
|
11
|
+
const pngBuffer = await generatePng(Buffer.from(sampleSvg), { width: 192, height: 192 })
|
|
12
|
+
expect(pngBuffer).toBeInstanceOf(Buffer)
|
|
13
|
+
|
|
14
|
+
const meta = await sharp(pngBuffer).metadata()
|
|
15
|
+
expect(meta.format).toBe("png")
|
|
16
|
+
expect(meta.width).toBe(192)
|
|
17
|
+
expect(meta.height).toBe(192)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("generates maskable PNG buffer with padding and background", async () => {
|
|
21
|
+
const maskableBuffer = await generateMaskablePng(Buffer.from(sampleSvg), {
|
|
22
|
+
size: 512,
|
|
23
|
+
background: "#123456"
|
|
24
|
+
})
|
|
25
|
+
expect(maskableBuffer).toBeInstanceOf(Buffer)
|
|
26
|
+
|
|
27
|
+
const meta = await sharp(maskableBuffer).metadata()
|
|
28
|
+
expect(meta.format).toBe("png")
|
|
29
|
+
expect(meta.width).toBe(512)
|
|
30
|
+
expect(meta.height).toBe(512)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("generates valid .ico buffer containing PNG data", async () => {
|
|
34
|
+
const icoBuffer = await generateIco(Buffer.from(sampleSvg), 32)
|
|
35
|
+
expect(icoBuffer).toBeInstanceOf(Buffer)
|
|
36
|
+
expect(icoBuffer.length).toBeGreaterThan(22)
|
|
37
|
+
|
|
38
|
+
// Verify ICO header
|
|
39
|
+
expect(icoBuffer.readUInt16LE(0)).toBe(0) // Reserved
|
|
40
|
+
expect(icoBuffer.readUInt16LE(2)).toBe(1) // Type = 1 (ICO)
|
|
41
|
+
expect(icoBuffer.readUInt16LE(4)).toBe(1) // 1 image
|
|
42
|
+
|
|
43
|
+
// Verify directory entry
|
|
44
|
+
expect(icoBuffer.readUInt8(6)).toBe(32) // Width
|
|
45
|
+
expect(icoBuffer.readUInt8(7)).toBe(32) // Height
|
|
46
|
+
expect(icoBuffer.readUInt16LE(10)).toBe(1) // Planes
|
|
47
|
+
expect(icoBuffer.readUInt16LE(12)).toBe(32) // 32 bpp
|
|
48
|
+
expect(icoBuffer.readUInt32LE(18)).toBe(22) // Offset = 22
|
|
49
|
+
|
|
50
|
+
// Verify PNG payload inside ICO
|
|
51
|
+
const pngSlice = icoBuffer.subarray(22)
|
|
52
|
+
const meta = await sharp(pngSlice).metadata()
|
|
53
|
+
expect(meta.format).toBe("png")
|
|
54
|
+
expect(meta.width).toBe(32)
|
|
55
|
+
expect(meta.height).toBe(32)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("generates standard 1200x630 OG card buffer", async () => {
|
|
59
|
+
const ogBuffer = await generateOgCard({
|
|
60
|
+
logoBuffer: Buffer.from(sampleSvg),
|
|
61
|
+
background: "#0a0a0a"
|
|
62
|
+
})
|
|
63
|
+
expect(ogBuffer).toBeInstanceOf(Buffer)
|
|
64
|
+
|
|
65
|
+
const meta = await sharp(ogBuffer).metadata()
|
|
66
|
+
expect(meta.format).toBe("png")
|
|
67
|
+
expect(meta.width).toBe(1200)
|
|
68
|
+
expect(meta.height).toBe(630)
|
|
69
|
+
})
|
|
70
|
+
})
|