@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,39 @@
|
|
|
1
|
+
//#region src/assets.d.ts
|
|
2
|
+
export interface GeneratePngOptions {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
background?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface GenerateMaskableOptions {
|
|
8
|
+
size?: number;
|
|
9
|
+
background?: string;
|
|
10
|
+
paddingRatio?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface GenerateOgCardOptions {
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
background?: string;
|
|
16
|
+
logoBuffer?: Buffer | Uint8Array | string | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generate a PNG buffer resized to specific dimensions from an SVG or image buffer.
|
|
20
|
+
*/
|
|
21
|
+
export declare function generatePng(source: Buffer | Uint8Array | string, options: {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}): Promise<Buffer>;
|
|
25
|
+
/**
|
|
26
|
+
* Generate a maskable PWA PNG icon with safe-zone padding and solid background. The standard
|
|
27
|
+
* recommendation is to place the master icon in the inner 80% safe zone.
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateMaskablePng(source: Buffer | Uint8Array | string, options?: GenerateMaskableOptions): Promise<Buffer>;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a valid .ico buffer containing a 32x32 PNG image according to the Windows ICO
|
|
32
|
+
* specification.
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateIco(source: Buffer | Uint8Array | string, size?: number): Promise<Buffer>;
|
|
35
|
+
/**
|
|
36
|
+
* Generate a standard 1200x630 branded Open Graph card from a master logo and background color.
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateOgCard(options?: GenerateOgCardOptions): Promise<Buffer>;
|
|
39
|
+
//#endregion
|
package/dist/assets.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
//#region src/assets.ts
|
|
3
|
+
/**
|
|
4
|
+
* Generate a PNG buffer resized to specific dimensions from an SVG or image buffer.
|
|
5
|
+
*/
|
|
6
|
+
async function generatePng(source, options) {
|
|
7
|
+
return sharp(source).resize(options.width, options.height, {
|
|
8
|
+
fit: "contain",
|
|
9
|
+
background: {
|
|
10
|
+
r: 0,
|
|
11
|
+
g: 0,
|
|
12
|
+
b: 0,
|
|
13
|
+
alpha: 0
|
|
14
|
+
}
|
|
15
|
+
}).png().toBuffer();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate a maskable PWA PNG icon with safe-zone padding and solid background. The standard
|
|
19
|
+
* recommendation is to place the master icon in the inner 80% safe zone.
|
|
20
|
+
*/
|
|
21
|
+
async function generateMaskablePng(source, options = {}) {
|
|
22
|
+
const { size = 512, background = "#ffffff", paddingRatio = .15 } = options;
|
|
23
|
+
const innerSize = Math.round(size * (1 - paddingRatio * 2));
|
|
24
|
+
const pad = Math.floor((size - innerSize) / 2);
|
|
25
|
+
const innerPng = await sharp(source).resize(innerSize, innerSize, {
|
|
26
|
+
fit: "contain",
|
|
27
|
+
background: {
|
|
28
|
+
r: 0,
|
|
29
|
+
g: 0,
|
|
30
|
+
b: 0,
|
|
31
|
+
alpha: 0
|
|
32
|
+
}
|
|
33
|
+
}).png().toBuffer();
|
|
34
|
+
return sharp(innerPng).extend({
|
|
35
|
+
top: pad,
|
|
36
|
+
bottom: size - innerSize - pad,
|
|
37
|
+
left: pad,
|
|
38
|
+
right: size - innerSize - pad,
|
|
39
|
+
background
|
|
40
|
+
}).png().toBuffer();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generate a valid .ico buffer containing a 32x32 PNG image according to the Windows ICO
|
|
44
|
+
* specification.
|
|
45
|
+
*/
|
|
46
|
+
async function generateIco(source, size = 32) {
|
|
47
|
+
const pngBuffer = await generatePng(source, {
|
|
48
|
+
width: size,
|
|
49
|
+
height: size
|
|
50
|
+
});
|
|
51
|
+
const header = Buffer.alloc(22);
|
|
52
|
+
header.writeUInt16LE(0, 0);
|
|
53
|
+
header.writeUInt16LE(1, 2);
|
|
54
|
+
header.writeUInt16LE(1, 4);
|
|
55
|
+
header.writeUInt8(size === 256 ? 0 : size, 6);
|
|
56
|
+
header.writeUInt8(size === 256 ? 0 : size, 7);
|
|
57
|
+
header.writeUInt8(0, 8);
|
|
58
|
+
header.writeUInt8(0, 9);
|
|
59
|
+
header.writeUInt16LE(1, 10);
|
|
60
|
+
header.writeUInt16LE(32, 12);
|
|
61
|
+
header.writeUInt32LE(pngBuffer.length, 14);
|
|
62
|
+
header.writeUInt32LE(22, 18);
|
|
63
|
+
return Buffer.concat([header, pngBuffer]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate a standard 1200x630 branded Open Graph card from a master logo and background color.
|
|
67
|
+
*/
|
|
68
|
+
async function generateOgCard(options = {}) {
|
|
69
|
+
const { width = 1200, height = 630, background = "#0a0a0a", logoBuffer } = options;
|
|
70
|
+
if (logoBuffer) {
|
|
71
|
+
const logoSize = Math.min(Math.round(height * .45), 280);
|
|
72
|
+
const logoPng = await generatePng(logoBuffer, {
|
|
73
|
+
width: logoSize,
|
|
74
|
+
height: logoSize
|
|
75
|
+
});
|
|
76
|
+
const topPad = Math.floor((height - logoSize) / 2);
|
|
77
|
+
const bottomPad = height - logoSize - topPad;
|
|
78
|
+
const leftPad = Math.floor((width - logoSize) / 2);
|
|
79
|
+
const rightPad = width - logoSize - leftPad;
|
|
80
|
+
return sharp(logoPng).extend({
|
|
81
|
+
top: topPad,
|
|
82
|
+
bottom: bottomPad,
|
|
83
|
+
left: leftPad,
|
|
84
|
+
right: rightPad,
|
|
85
|
+
background
|
|
86
|
+
}).png().toBuffer();
|
|
87
|
+
}
|
|
88
|
+
return sharp({ create: {
|
|
89
|
+
width,
|
|
90
|
+
height,
|
|
91
|
+
channels: 4,
|
|
92
|
+
background
|
|
93
|
+
} }).png().toBuffer();
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { generateIco, generateMaskablePng, generateOgCard, generatePng };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ApiCatalog, ApiCatalogOptions } from "./types.mjs";
|
|
2
|
+
//#region src/catalog.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Build RFC 9652 Application Linkset API Catalog. https://www.rfc-editor.org/rfc/rfc9652
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildApiCatalog(options: ApiCatalogOptions): ApiCatalog;
|
|
7
|
+
//#endregion
|
package/dist/catalog.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/catalog.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build RFC 9652 Application Linkset API Catalog. https://www.rfc-editor.org/rfc/rfc9652
|
|
4
|
+
*/
|
|
5
|
+
function buildApiCatalog(options) {
|
|
6
|
+
const { items } = options;
|
|
7
|
+
return { "api-catalog": items };
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { buildApiCatalog };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { generateIco, generateMaskablePng, generateOgCard, generatePng } from "./assets.mjs";
|
|
1
2
|
import { i as buildBreadcrumbSchema, n as BreadcrumbItem, r as buildArticleSchema, t as ArticleSchemaOptions } from "./schema-C8v69blQ.mjs";
|
|
2
3
|
import { a as buildLlmsTxt, i as buildLlmsFullTxt, n as LlmsPage, r as LlmsTxtOptions, t as LlmsFullTxtOptions } from "./llms-BVLxzjr-.mjs";
|
|
3
|
-
import { ChangeFreq, HeadProps, LlmsFullRouteOptions, LlmsRouteOptions, LocaleConfig, OGImage, OpenGraphMeta, RSSFeed, RimelightSeoOptions, RobotsOptions, RobotsRouteOptions, RssFeedOptions, RssItem, RssOptions, SeoEntry, SiteConfig, SitemapAlternate, SitemapOptions, SitemapUrl, TwitterMeta, UserAgentRule } from "./types.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";
|
|
5
|
+
import { buildApiCatalog } from "./catalog.mjs";
|
|
4
6
|
import { buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, chunkSitemapUrls } from "./sitemap.mjs";
|
|
5
7
|
import { buildRobotsTxt } from "./robots.mjs";
|
|
6
8
|
import { buildRssXml, escapeXml } from "./rss.mjs";
|
|
7
9
|
import { buildCanonicalUrl, buildPageTitle, buildRobotsMetaContent, buildSafeDescription, buildWebSiteSchema, deepMerge } from "./meta.mjs";
|
|
10
|
+
import { buildSecurityTxt } from "./security.mjs";
|
|
11
|
+
import { buildWebManifest } from "./manifest.mjs";
|
|
8
12
|
import { OgFontConfig, OgLayoutOptions, OgRenderConfig } from "./og.mjs";
|
|
9
13
|
import rimelightSeo from "./integration.mjs";
|
|
10
|
-
export { type ArticleSchemaOptions, type BreadcrumbItem, type ChangeFreq, type HeadProps, type LlmsFullRouteOptions, type LlmsFullTxtOptions, type LlmsPage, type LlmsRouteOptions, type LlmsTxtOptions, type LocaleConfig, type OGImage, type OgFontConfig, type OgLayoutOptions, type OgRenderConfig, type OpenGraphMeta, type RSSFeed, type RimelightSeoOptions, type RobotsOptions, type RobotsRouteOptions, type RssFeedOptions, type RssItem, type RssOptions, type SeoEntry, type SiteConfig, type SitemapAlternate, type SitemapOptions, type SitemapUrl, type TwitterMeta, type UserAgentRule, buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebSiteSchema, chunkSitemapUrls, deepMerge, rimelightSeo as default, rimelightSeo, escapeXml };
|
|
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { generateIco, generateMaskablePng, generateOgCard, generatePng } from "./assets.mjs";
|
|
2
|
+
import { buildApiCatalog } from "./catalog.mjs";
|
|
1
3
|
import { buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, chunkSitemapUrls } from "./sitemap.mjs";
|
|
2
4
|
import { buildRobotsTxt } from "./robots.mjs";
|
|
3
5
|
import { buildRssXml, escapeXml } from "./rss.mjs";
|
|
4
6
|
import { buildCanonicalUrl, buildPageTitle, buildRobotsMetaContent, buildSafeDescription, buildWebSiteSchema, deepMerge } from "./meta.mjs";
|
|
5
7
|
import { buildLlmsFullTxt, buildLlmsTxt } from "./llms.mjs";
|
|
8
|
+
import { buildSecurityTxt } from "./security.mjs";
|
|
9
|
+
import { buildWebManifest } from "./manifest.mjs";
|
|
6
10
|
import { buildArticleSchema, buildBreadcrumbSchema } from "./schema.mjs";
|
|
7
11
|
import rimelightSeo from "./integration.mjs";
|
|
8
12
|
//#region src/index.ts
|
|
9
13
|
var src_default = rimelightSeo;
|
|
10
14
|
//#endregion
|
|
11
|
-
export { buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebSiteSchema, chunkSitemapUrls, deepMerge, src_default as default, escapeXml, rimelightSeo };
|
|
15
|
+
export { buildApiCatalog, buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSecurityTxt, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebManifest, buildWebSiteSchema, chunkSitemapUrls, deepMerge, src_default as default, escapeXml, generateIco, generateMaskablePng, generateOgCard, generatePng, rimelightSeo };
|
package/dist/integration.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generateIco, generateMaskablePng, generateOgCard, generatePng } from "./assets.mjs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,6 +20,11 @@ function rimelightSeo(options = {}) {
|
|
|
19
20
|
"src/config/seo.config.js",
|
|
20
21
|
"src/config/seo.config.mjs"
|
|
21
22
|
]);
|
|
23
|
+
const hasConfigExport = (name) => {
|
|
24
|
+
if (!seoConfigPath) return false;
|
|
25
|
+
const content = fs.readFileSync(seoConfigPath, "utf8");
|
|
26
|
+
return new RegExp(`export\\s+(?:const|let|var|async\\s+function|function|class)\\s+${name}\\b`).test(content) || new RegExp(`export\\s*\\{[^}]*\\b${name}\\b}`).test(content);
|
|
27
|
+
};
|
|
22
28
|
const siteConfigPath = findFile([
|
|
23
29
|
"src/config/site.config.ts",
|
|
24
30
|
"src/config/site.config.js",
|
|
@@ -34,6 +40,25 @@ function rimelightSeo(options = {}) {
|
|
|
34
40
|
"src/config/llms.corpus.js",
|
|
35
41
|
"src/config/llms.corpus.mjs"
|
|
36
42
|
]);
|
|
43
|
+
const assetOpts = typeof options.assets === "object" ? options.assets : {};
|
|
44
|
+
const explicitIcon = assetOpts.icon ? path.resolve(rootDir, assetOpts.icon) : null;
|
|
45
|
+
const masterIconPath = explicitIcon && fs.existsSync(explicitIcon) ? explicitIcon : findFile([
|
|
46
|
+
"src/assets/logos/logomark_color.svg",
|
|
47
|
+
"src/assets/logos/logo_color.svg",
|
|
48
|
+
"src/assets/logos/logomark.svg",
|
|
49
|
+
"src/assets/logos/logo.svg",
|
|
50
|
+
"src/assets/icon.svg",
|
|
51
|
+
"src/assets/logo.svg",
|
|
52
|
+
"src/assets/icon.png",
|
|
53
|
+
"public/favicon.svg"
|
|
54
|
+
]);
|
|
55
|
+
let masterIconBuffer = null;
|
|
56
|
+
let isSvg = false;
|
|
57
|
+
if (masterIconPath && fs.existsSync(masterIconPath)) {
|
|
58
|
+
masterIconBuffer = fs.readFileSync(masterIconPath);
|
|
59
|
+
isSvg = masterIconPath.endsWith(".svg");
|
|
60
|
+
}
|
|
61
|
+
const bgColor = assetOpts.backgroundColor || "#ffffff";
|
|
37
62
|
if (options.robots !== false) injectRoute({
|
|
38
63
|
pattern: typeof options.robots === "object" && options.robots.path ? options.robots.path : "/robots.txt",
|
|
39
64
|
entrypoint: "@rimelight/seo/routes/robots.txt.ts"
|
|
@@ -54,7 +79,169 @@ function rimelightSeo(options = {}) {
|
|
|
54
79
|
pattern: typeof options.llmsFull === "object" && options.llmsFull.path ? options.llmsFull.path : "/llms-full.txt",
|
|
55
80
|
entrypoint: "@rimelight/seo/routes/llms-full.txt.ts"
|
|
56
81
|
});
|
|
82
|
+
if (options.security !== false) {
|
|
83
|
+
const pattern = typeof options.security === "object" && options.security.path ? options.security.path : "/.well-known/security.txt";
|
|
84
|
+
injectRoute({
|
|
85
|
+
pattern,
|
|
86
|
+
entrypoint: "@rimelight/seo/routes/security.txt.ts"
|
|
87
|
+
});
|
|
88
|
+
if ((typeof options.security === "object" ? options.security.legacyRoute !== false : true) && pattern === "/.well-known/security.txt") injectRoute({
|
|
89
|
+
pattern: "/security.txt",
|
|
90
|
+
entrypoint: "@rimelight/seo/routes/security.txt.ts"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (options.apiCatalog !== false) injectRoute({
|
|
94
|
+
pattern: typeof options.apiCatalog === "object" && options.apiCatalog.path ? options.apiCatalog.path : "/.well-known/api-catalog",
|
|
95
|
+
entrypoint: "@rimelight/seo/routes/api-catalog.ts"
|
|
96
|
+
});
|
|
97
|
+
if (options.manifest !== false) injectRoute({
|
|
98
|
+
pattern: typeof options.manifest === "object" && options.manifest.path ? options.manifest.path : "/site.webmanifest",
|
|
99
|
+
entrypoint: "@rimelight/seo/routes/site.webmanifest.ts"
|
|
100
|
+
});
|
|
57
101
|
updateConfig({ vite: { plugins: [{
|
|
102
|
+
name: "vite-plugin-rimelight-seo-assets",
|
|
103
|
+
configureServer(server) {
|
|
104
|
+
if (!masterIconBuffer) return;
|
|
105
|
+
server.middlewares.use(async (req, res, next) => {
|
|
106
|
+
const url = req.url?.split("?")[0];
|
|
107
|
+
if (url === "/favicon.ico" && assetOpts.faviconIco !== false) {
|
|
108
|
+
const ico = await generateIco(masterIconBuffer, 32);
|
|
109
|
+
res.setHeader("Content-Type", "image/x-icon");
|
|
110
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
111
|
+
res.end(ico);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (url === "/apple-touch-icon.png" && assetOpts.appleTouchIcon !== false) {
|
|
115
|
+
const png = await generatePng(masterIconBuffer, {
|
|
116
|
+
width: 180,
|
|
117
|
+
height: 180
|
|
118
|
+
});
|
|
119
|
+
res.setHeader("Content-Type", "image/png");
|
|
120
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
121
|
+
res.end(png);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (url === "/icon-192.png" && assetOpts.icon192 !== false) {
|
|
125
|
+
const png = await generatePng(masterIconBuffer, {
|
|
126
|
+
width: 192,
|
|
127
|
+
height: 192
|
|
128
|
+
});
|
|
129
|
+
res.setHeader("Content-Type", "image/png");
|
|
130
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
131
|
+
res.end(png);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (url === "/icon-512.png" && assetOpts.icon512 !== false) {
|
|
135
|
+
const png = await generatePng(masterIconBuffer, {
|
|
136
|
+
width: 512,
|
|
137
|
+
height: 512
|
|
138
|
+
});
|
|
139
|
+
res.setHeader("Content-Type", "image/png");
|
|
140
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
141
|
+
res.end(png);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (url === "/icon-maskable-512.png" && assetOpts.iconMaskable !== false) {
|
|
145
|
+
const png = await generateMaskablePng(masterIconBuffer, {
|
|
146
|
+
size: 512,
|
|
147
|
+
background: bgColor
|
|
148
|
+
});
|
|
149
|
+
res.setHeader("Content-Type", "image/png");
|
|
150
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
151
|
+
res.end(png);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (url === "/favicon.svg" && isSvg) {
|
|
155
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
156
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
157
|
+
res.end(masterIconBuffer);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (url === "/og.png" && assetOpts.ogImage !== false) {
|
|
161
|
+
const og = await generateOgCard({
|
|
162
|
+
logoBuffer: masterIconBuffer,
|
|
163
|
+
background: bgColor
|
|
164
|
+
});
|
|
165
|
+
res.setHeader("Content-Type", "image/png");
|
|
166
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
167
|
+
res.end(og);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
next();
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
async generateBundle() {
|
|
174
|
+
if (options.assets === false || !masterIconBuffer) return;
|
|
175
|
+
if (assetOpts.faviconIco !== false) {
|
|
176
|
+
const ico = await generateIco(masterIconBuffer, 32);
|
|
177
|
+
this.emitFile({
|
|
178
|
+
type: "asset",
|
|
179
|
+
fileName: "favicon.ico",
|
|
180
|
+
source: ico
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (assetOpts.appleTouchIcon !== false) {
|
|
184
|
+
const png = await generatePng(masterIconBuffer, {
|
|
185
|
+
width: 180,
|
|
186
|
+
height: 180
|
|
187
|
+
});
|
|
188
|
+
this.emitFile({
|
|
189
|
+
type: "asset",
|
|
190
|
+
fileName: "apple-touch-icon.png",
|
|
191
|
+
source: png
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (assetOpts.icon192 !== false) {
|
|
195
|
+
const png = await generatePng(masterIconBuffer, {
|
|
196
|
+
width: 192,
|
|
197
|
+
height: 192
|
|
198
|
+
});
|
|
199
|
+
this.emitFile({
|
|
200
|
+
type: "asset",
|
|
201
|
+
fileName: "icon-192.png",
|
|
202
|
+
source: png
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (assetOpts.icon512 !== false) {
|
|
206
|
+
const png = await generatePng(masterIconBuffer, {
|
|
207
|
+
width: 512,
|
|
208
|
+
height: 512
|
|
209
|
+
});
|
|
210
|
+
this.emitFile({
|
|
211
|
+
type: "asset",
|
|
212
|
+
fileName: "icon-512.png",
|
|
213
|
+
source: png
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (assetOpts.iconMaskable !== false) {
|
|
217
|
+
const png = await generateMaskablePng(masterIconBuffer, {
|
|
218
|
+
size: 512,
|
|
219
|
+
background: bgColor
|
|
220
|
+
});
|
|
221
|
+
this.emitFile({
|
|
222
|
+
type: "asset",
|
|
223
|
+
fileName: "icon-maskable-512.png",
|
|
224
|
+
source: png
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (assetOpts.ogImage !== false) {
|
|
228
|
+
const og = await generateOgCard({
|
|
229
|
+
logoBuffer: masterIconBuffer,
|
|
230
|
+
background: bgColor
|
|
231
|
+
});
|
|
232
|
+
this.emitFile({
|
|
233
|
+
type: "asset",
|
|
234
|
+
fileName: "og.png",
|
|
235
|
+
source: og
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (isSvg) this.emitFile({
|
|
239
|
+
type: "asset",
|
|
240
|
+
fileName: "favicon.svg",
|
|
241
|
+
source: masterIconBuffer
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}, {
|
|
58
245
|
name: "vite-plugin-rimelight-seo",
|
|
59
246
|
resolveId(id) {
|
|
60
247
|
if (id === "virtual:rimelight-seo-config" || id === "virtual:rimelight-seo/options") return "\0virtual:rimelight-seo/options";
|
|
@@ -67,12 +254,16 @@ function rimelightSeo(options = {}) {
|
|
|
67
254
|
load(id) {
|
|
68
255
|
if (id === "\0virtual:rimelight-seo/options") return `export default ${JSON.stringify(options)};`;
|
|
69
256
|
if (id === "\0virtual:rimelight-seo/config") {
|
|
70
|
-
if (seoConfigPath)
|
|
257
|
+
if (seoConfigPath) {
|
|
258
|
+
const seoEntriesExport = hasConfigExport("seoEntries");
|
|
259
|
+
const rssEntriesExport = hasConfigExport("rssEntries");
|
|
260
|
+
return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
|
|
71
261
|
export const SEO_LOCALES = customConfig.SEO_LOCALES;
|
|
72
262
|
export const PRIVATE_PATH_PREFIXES = customConfig.PRIVATE_PATH_PREFIXES || [];
|
|
73
|
-
export const seoEntries = customConfig.seoEntries;
|
|
74
|
-
export const rssEntries = customConfig.rssEntries;
|
|
263
|
+
export const seoEntries = ${seoEntriesExport ? "customConfig.seoEntries" : "undefined"};
|
|
264
|
+
export const rssEntries = ${rssEntriesExport ? "customConfig.rssEntries" : "undefined"};
|
|
75
265
|
export default customConfig;`;
|
|
266
|
+
}
|
|
76
267
|
return `export const SEO_LOCALES = undefined;
|
|
77
268
|
export const PRIVATE_PATH_PREFIXES = [];
|
|
78
269
|
export const seoEntries = async () => [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { WebManifest, WebManifestOptions } from "./types.mjs";
|
|
2
|
+
//#region src/manifest.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Build web app manifest (site.webmanifest) compliant with W3C Web App Manifest.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildWebManifest(options: WebManifestOptions): WebManifest;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/manifest.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build web app manifest (site.webmanifest) compliant with W3C Web App Manifest.
|
|
4
|
+
*/
|
|
5
|
+
function buildWebManifest(options) {
|
|
6
|
+
const { name, shortName = name, description = "", startUrl = "/", display = "standalone", backgroundColor = "#ffffff", themeColor = "#ffffff", icons = [
|
|
7
|
+
{
|
|
8
|
+
src: "/icon-192.png",
|
|
9
|
+
sizes: "192x192",
|
|
10
|
+
type: "image/png",
|
|
11
|
+
purpose: "any"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
src: "/icon-512.png",
|
|
15
|
+
sizes: "512x512",
|
|
16
|
+
type: "image/png",
|
|
17
|
+
purpose: "any"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
src: "/icon-maskable-512.png",
|
|
21
|
+
sizes: "512x512",
|
|
22
|
+
type: "image/png",
|
|
23
|
+
purpose: "maskable"
|
|
24
|
+
}
|
|
25
|
+
], shortcuts, categories, lang, dir, orientation, scope } = options;
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
short_name: shortName,
|
|
29
|
+
...description ? { description } : {},
|
|
30
|
+
start_url: startUrl,
|
|
31
|
+
display,
|
|
32
|
+
background_color: backgroundColor,
|
|
33
|
+
theme_color: themeColor,
|
|
34
|
+
icons,
|
|
35
|
+
...shortcuts ? { shortcuts } : {},
|
|
36
|
+
...categories ? { categories } : {},
|
|
37
|
+
...lang ? { lang } : {},
|
|
38
|
+
...dir ? { dir } : {},
|
|
39
|
+
...orientation ? { orientation } : {},
|
|
40
|
+
...scope ? { scope } : {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { buildWebManifest };
|
package/dist/og.mjs
CHANGED
|
@@ -45,7 +45,7 @@ async function loadOgFonts(config = NOTO_SANS, timeoutMs = 5e3) {
|
|
|
45
45
|
* `renderOgResponse()` directly -- this function is just the house style.
|
|
46
46
|
*/
|
|
47
47
|
function defaultOgLayout(options, font = NOTO_SANS) {
|
|
48
|
-
const { title
|
|
48
|
+
const { title, description, brand, badge, date, background = "dark", preview = false } = options;
|
|
49
49
|
const MAX_TITLE_LEN = 160;
|
|
50
50
|
const MAX_DESC_LEN = 240;
|
|
51
51
|
const safeTitle = title.length > MAX_TITLE_LEN ? title.slice(0, 159) + "…" : title;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { buildApiCatalog } from "../catalog.mjs";
|
|
2
|
+
import seoOptions from "virtual:rimelight-seo/options";
|
|
3
|
+
//#region src/routes/api-catalog.ts
|
|
4
|
+
const prerender = false;
|
|
5
|
+
const GET = () => {
|
|
6
|
+
const catalogOpts = typeof seoOptions?.apiCatalog === "object" ? seoOptions.apiCatalog : {};
|
|
7
|
+
const defaultItems = [{
|
|
8
|
+
rel: "self",
|
|
9
|
+
href: "/.well-known/api-catalog",
|
|
10
|
+
type: "application/linkset+json"
|
|
11
|
+
}];
|
|
12
|
+
if (seoOptions?.sitemap !== false) defaultItems.push({
|
|
13
|
+
rel: "alternate",
|
|
14
|
+
href: "/sitemap.xml",
|
|
15
|
+
type: "application/xml",
|
|
16
|
+
title: "Sitemap"
|
|
17
|
+
});
|
|
18
|
+
if (seoOptions?.rss !== false) defaultItems.push({
|
|
19
|
+
rel: "alternate",
|
|
20
|
+
href: "/rss.xml",
|
|
21
|
+
type: "application/rss+xml",
|
|
22
|
+
title: "RSS Feed"
|
|
23
|
+
});
|
|
24
|
+
if (seoOptions?.llms !== false) defaultItems.push({
|
|
25
|
+
rel: "llms",
|
|
26
|
+
href: "/llms.txt",
|
|
27
|
+
type: "text/plain",
|
|
28
|
+
title: "LLMs.txt"
|
|
29
|
+
});
|
|
30
|
+
const items = catalogOpts.items || defaultItems;
|
|
31
|
+
const catalog = buildApiCatalog({ items });
|
|
32
|
+
return new Response(JSON.stringify(catalog, null, 2), { headers: {
|
|
33
|
+
"Content-Type": "application/linkset+json; charset=utf-8",
|
|
34
|
+
"Cache-Control": "public, max-age=86400, stale-while-revalidate=604800"
|
|
35
|
+
} });
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { GET, prerender };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { generatePng } from "../assets.mjs";
|
|
2
|
+
import { masterIconBuffer } from "virtual:rimelight-seo/assets";
|
|
3
|
+
//#region src/routes/apple-touch-icon.png.ts
|
|
4
|
+
const prerender = true;
|
|
5
|
+
const GET = async () => {
|
|
6
|
+
if (!masterIconBuffer) return new Response(null, { status: 404 });
|
|
7
|
+
const png = await generatePng(masterIconBuffer, {
|
|
8
|
+
width: 180,
|
|
9
|
+
height: 180
|
|
10
|
+
});
|
|
11
|
+
return new Response(new Uint8Array(png), { headers: {
|
|
12
|
+
"Content-Type": "image/png",
|
|
13
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
14
|
+
} });
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { GET, prerender };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { generateIco } from "../assets.mjs";
|
|
2
|
+
import { masterIconBuffer } from "virtual:rimelight-seo/assets";
|
|
3
|
+
//#region src/routes/favicon.ico.ts
|
|
4
|
+
const prerender = true;
|
|
5
|
+
const GET = async () => {
|
|
6
|
+
if (!masterIconBuffer) return new Response(null, { status: 404 });
|
|
7
|
+
const ico = await generateIco(masterIconBuffer, 32);
|
|
8
|
+
return new Response(new Uint8Array(ico), { headers: {
|
|
9
|
+
"Content-Type": "image/x-icon",
|
|
10
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
11
|
+
} });
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { GET, prerender };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isSvg, masterIconBuffer } from "virtual:rimelight-seo/assets";
|
|
2
|
+
//#region src/routes/favicon.svg.ts
|
|
3
|
+
const prerender = true;
|
|
4
|
+
const GET = async () => {
|
|
5
|
+
if (!masterIconBuffer || !isSvg) return new Response(null, { status: 404 });
|
|
6
|
+
return new Response(new Uint8Array(masterIconBuffer), { headers: {
|
|
7
|
+
"Content-Type": "image/svg+xml",
|
|
8
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
9
|
+
} });
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { GET, prerender };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { generatePng } from "../assets.mjs";
|
|
2
|
+
import { masterIconBuffer } from "virtual:rimelight-seo/assets";
|
|
3
|
+
//#region src/routes/icon-192.png.ts
|
|
4
|
+
const prerender = true;
|
|
5
|
+
const GET = async () => {
|
|
6
|
+
if (!masterIconBuffer) return new Response(null, { status: 404 });
|
|
7
|
+
const png = await generatePng(masterIconBuffer, {
|
|
8
|
+
width: 192,
|
|
9
|
+
height: 192
|
|
10
|
+
});
|
|
11
|
+
return new Response(new Uint8Array(png), { headers: {
|
|
12
|
+
"Content-Type": "image/png",
|
|
13
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
14
|
+
} });
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { GET, prerender };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { generatePng } from "../assets.mjs";
|
|
2
|
+
import { masterIconBuffer } from "virtual:rimelight-seo/assets";
|
|
3
|
+
//#region src/routes/icon-512.png.ts
|
|
4
|
+
const prerender = true;
|
|
5
|
+
const GET = async () => {
|
|
6
|
+
if (!masterIconBuffer) return new Response(null, { status: 404 });
|
|
7
|
+
const png = await generatePng(masterIconBuffer, {
|
|
8
|
+
width: 512,
|
|
9
|
+
height: 512
|
|
10
|
+
});
|
|
11
|
+
return new Response(new Uint8Array(png), { headers: {
|
|
12
|
+
"Content-Type": "image/png",
|
|
13
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
14
|
+
} });
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { GET, prerender };
|