@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
package/src/assets.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import sharp from "sharp"
|
|
2
|
+
|
|
3
|
+
export interface GeneratePngOptions {
|
|
4
|
+
width: number
|
|
5
|
+
height: number
|
|
6
|
+
background?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface GenerateMaskableOptions {
|
|
10
|
+
size?: number
|
|
11
|
+
background?: string
|
|
12
|
+
paddingRatio?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GenerateOgCardOptions {
|
|
16
|
+
width?: number
|
|
17
|
+
height?: number
|
|
18
|
+
background?: string
|
|
19
|
+
logoBuffer?: Buffer | Uint8Array | string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate a PNG buffer resized to specific dimensions from an SVG or image buffer.
|
|
24
|
+
*/
|
|
25
|
+
export async function generatePng(
|
|
26
|
+
source: Buffer | Uint8Array | string,
|
|
27
|
+
options: { width: number; height: number }
|
|
28
|
+
): Promise<Buffer> {
|
|
29
|
+
return sharp(source)
|
|
30
|
+
.resize(options.width, options.height, {
|
|
31
|
+
fit: "contain",
|
|
32
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
33
|
+
})
|
|
34
|
+
.png()
|
|
35
|
+
.toBuffer()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate a maskable PWA PNG icon with safe-zone padding and solid background. The standard
|
|
40
|
+
* recommendation is to place the master icon in the inner 80% safe zone.
|
|
41
|
+
*/
|
|
42
|
+
export async function generateMaskablePng(
|
|
43
|
+
source: Buffer | Uint8Array | string,
|
|
44
|
+
options: GenerateMaskableOptions = {}
|
|
45
|
+
): Promise<Buffer> {
|
|
46
|
+
const { size = 512, background = "#ffffff", paddingRatio = 0.15 } = options
|
|
47
|
+
const innerSize = Math.round(size * (1 - paddingRatio * 2))
|
|
48
|
+
const pad = Math.floor((size - innerSize) / 2)
|
|
49
|
+
|
|
50
|
+
const innerPng = await sharp(source)
|
|
51
|
+
.resize(innerSize, innerSize, {
|
|
52
|
+
fit: "contain",
|
|
53
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
54
|
+
})
|
|
55
|
+
.png()
|
|
56
|
+
.toBuffer()
|
|
57
|
+
|
|
58
|
+
return sharp(innerPng)
|
|
59
|
+
.extend({
|
|
60
|
+
top: pad,
|
|
61
|
+
bottom: size - innerSize - pad,
|
|
62
|
+
left: pad,
|
|
63
|
+
right: size - innerSize - pad,
|
|
64
|
+
background
|
|
65
|
+
})
|
|
66
|
+
.png()
|
|
67
|
+
.toBuffer()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a valid .ico buffer containing a 32x32 PNG image according to the Windows ICO
|
|
72
|
+
* specification.
|
|
73
|
+
*/
|
|
74
|
+
export async function generateIco(
|
|
75
|
+
source: Buffer | Uint8Array | string,
|
|
76
|
+
size = 32
|
|
77
|
+
): Promise<Buffer> {
|
|
78
|
+
const pngBuffer = await generatePng(source, { width: size, height: size })
|
|
79
|
+
|
|
80
|
+
const header = Buffer.alloc(22)
|
|
81
|
+
// Reserved (2 bytes)
|
|
82
|
+
header.writeUInt16LE(0, 0)
|
|
83
|
+
// Image type (1 = ICO) (2 bytes)
|
|
84
|
+
header.writeUInt16LE(1, 2)
|
|
85
|
+
// Number of images (2 bytes)
|
|
86
|
+
header.writeUInt16LE(1, 4)
|
|
87
|
+
|
|
88
|
+
// Directory entry (16 bytes)
|
|
89
|
+
header.writeUInt8(size === 256 ? 0 : size, 6) // Width
|
|
90
|
+
header.writeUInt8(size === 256 ? 0 : size, 7) // Height
|
|
91
|
+
header.writeUInt8(0, 8) // Palette size (0 = no palette)
|
|
92
|
+
header.writeUInt8(0, 9) // Reserved
|
|
93
|
+
header.writeUInt16LE(1, 10) // Color planes
|
|
94
|
+
header.writeUInt16LE(32, 12) // Bits per pixel (32bpp)
|
|
95
|
+
header.writeUInt32LE(pngBuffer.length, 14) // Image size in bytes
|
|
96
|
+
header.writeUInt32LE(22, 18) // Offset of image data (header size = 22)
|
|
97
|
+
|
|
98
|
+
return Buffer.concat([header, pngBuffer])
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate a standard 1200x630 branded Open Graph card from a master logo and background color.
|
|
103
|
+
*/
|
|
104
|
+
export async function generateOgCard(options: GenerateOgCardOptions = {}): Promise<Buffer> {
|
|
105
|
+
const { width = 1200, height = 630, background = "#0a0a0a", logoBuffer } = options
|
|
106
|
+
|
|
107
|
+
if (logoBuffer) {
|
|
108
|
+
const logoSize = Math.min(Math.round(height * 0.45), 280)
|
|
109
|
+
const logoPng = await generatePng(logoBuffer, { width: logoSize, height: logoSize })
|
|
110
|
+
const topPad = Math.floor((height - logoSize) / 2)
|
|
111
|
+
const bottomPad = height - logoSize - topPad
|
|
112
|
+
const leftPad = Math.floor((width - logoSize) / 2)
|
|
113
|
+
const rightPad = width - logoSize - leftPad
|
|
114
|
+
|
|
115
|
+
return sharp(logoPng)
|
|
116
|
+
.extend({
|
|
117
|
+
top: topPad,
|
|
118
|
+
bottom: bottomPad,
|
|
119
|
+
left: leftPad,
|
|
120
|
+
right: rightPad,
|
|
121
|
+
background
|
|
122
|
+
})
|
|
123
|
+
.png()
|
|
124
|
+
.toBuffer()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return sharp({
|
|
128
|
+
create: {
|
|
129
|
+
width,
|
|
130
|
+
height,
|
|
131
|
+
channels: 4,
|
|
132
|
+
background
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.png()
|
|
136
|
+
.toBuffer()
|
|
137
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from "vite-plus/test"
|
|
2
|
+
import { buildApiCatalog } from "./catalog.js"
|
|
3
|
+
|
|
4
|
+
describe("buildApiCatalog", () => {
|
|
5
|
+
it("builds linkset api-catalog object", () => {
|
|
6
|
+
const catalog = buildApiCatalog({
|
|
7
|
+
items: [
|
|
8
|
+
{
|
|
9
|
+
rel: "self",
|
|
10
|
+
href: "/.well-known/api-catalog",
|
|
11
|
+
type: "application/linkset+json"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
rel: "alternate",
|
|
15
|
+
href: "/sitemap.xml",
|
|
16
|
+
type: "application/xml"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const items = catalog["api-catalog"]
|
|
22
|
+
expect(items).toHaveLength(2)
|
|
23
|
+
expect(items[0]?.rel).toBe("self")
|
|
24
|
+
expect(items[1]?.href).toBe("/sitemap.xml")
|
|
25
|
+
})
|
|
26
|
+
})
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ApiCatalog, ApiCatalogOptions } from "./types.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build RFC 9652 Application Linkset API Catalog. https://www.rfc-editor.org/rfc/rfc9652
|
|
5
|
+
*/
|
|
6
|
+
export function buildApiCatalog(options: ApiCatalogOptions): ApiCatalog {
|
|
7
|
+
const { items } = options
|
|
8
|
+
return {
|
|
9
|
+
"api-catalog": items
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -39,7 +39,7 @@ const {
|
|
|
39
39
|
// Resolve defaults from config if available
|
|
40
40
|
const siteName = config?.name || 'Rimelight'
|
|
41
41
|
const siteDescription = config?.description || ''
|
|
42
|
-
const ogImageFallback = config?.seo?.ogImageFallback || config?.ogImage || ''
|
|
42
|
+
const ogImageFallback = config?.seo?.ogImageFallback || config?.ogImage || '/og.png'
|
|
43
43
|
const themeColor = config?.branding?.colors?.themeColor
|
|
44
44
|
const favicon = customFavicon || config?.branding?.favicon?.svg || '/favicon.svg'
|
|
45
45
|
const faviconType = favicon.endsWith('.ico')
|
package/src/env.d.ts
CHANGED
|
@@ -1,33 +1,55 @@
|
|
|
1
|
-
declare module "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
declare module "virtual:rimelight-seo/
|
|
23
|
-
export const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export const
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
declare module "
|
|
32
|
-
export const
|
|
33
|
-
}
|
|
1
|
+
declare module "virtual:rimelight-seo-config" {
|
|
2
|
+
const config: import("./integration.js").RimelightSeoOptions
|
|
3
|
+
export default config
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module "virtual:rimelight-seo/options" {
|
|
7
|
+
const config: import("./integration.js").RimelightSeoOptions
|
|
8
|
+
export default config
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "virtual:rimelight-seo/config" {
|
|
12
|
+
export const SEO_LOCALES: import("./types.js").LocaleConfig | undefined
|
|
13
|
+
export const PRIVATE_PATH_PREFIXES: string[] | undefined
|
|
14
|
+
export const seoEntries: (() => Promise<import("./types.js").SeoEntry[]>) | undefined
|
|
15
|
+
export const rssEntries: (() => Promise<import("./types.js").RssItem[]>) | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare module "virtual:rimelight-seo/site-config" {
|
|
19
|
+
export const siteConfig: import("./types.js").SiteConfig | Record<string, any>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module "virtual:rimelight-seo/db" {
|
|
23
|
+
export const db: any
|
|
24
|
+
export const pages: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare module "virtual:rimelight-seo/corpus" {
|
|
28
|
+
export const corpusPages: any[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare module "#db/schema" {
|
|
32
|
+
export const pages: any
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare module "drizzle-orm" {
|
|
36
|
+
export const and: (...args: any[]) => any
|
|
37
|
+
export const isNull: (column: any) => any
|
|
38
|
+
export const isNotNull: (column: any) => any
|
|
39
|
+
export const sql: any
|
|
40
|
+
export const eq: (column: any, value: any) => any
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare module "@rimelight/cms" {
|
|
44
|
+
export const renderCorpusMarkdown: (db: any, options: any) => Promise<string>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare module "astro:content" {
|
|
48
|
+
export const getCollection: (collection: string) => Promise<any[]>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
declare module "virtual:rimelight-seo/assets" {
|
|
52
|
+
export const masterIconBuffer: Buffer | null
|
|
53
|
+
export const isSvg: boolean
|
|
54
|
+
export const backgroundColor: string
|
|
55
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type {
|
|
|
14
14
|
RssItem,
|
|
15
15
|
RssFeedOptions,
|
|
16
16
|
RssOptions,
|
|
17
|
+
RssCollectionConfig,
|
|
17
18
|
HeadProps,
|
|
18
19
|
OpenGraphMeta,
|
|
19
20
|
TwitterMeta,
|
|
@@ -23,7 +24,19 @@ export type {
|
|
|
23
24
|
LlmsRouteOptions,
|
|
24
25
|
LlmsFullRouteOptions,
|
|
25
26
|
ArticleSchemaOptions,
|
|
26
|
-
BreadcrumbItem
|
|
27
|
+
BreadcrumbItem,
|
|
28
|
+
SecurityTxtOptions,
|
|
29
|
+
SecurityRouteOptions,
|
|
30
|
+
WebManifestIcon,
|
|
31
|
+
WebManifestShortcut,
|
|
32
|
+
WebManifest,
|
|
33
|
+
WebManifestOptions,
|
|
34
|
+
ManifestRouteOptions,
|
|
35
|
+
ApiCatalogItem,
|
|
36
|
+
ApiCatalog,
|
|
37
|
+
ApiCatalogOptions,
|
|
38
|
+
ApiCatalogRouteOptions,
|
|
39
|
+
AssetRouteOptions
|
|
27
40
|
} from "./types.js"
|
|
28
41
|
|
|
29
42
|
export {
|
|
@@ -47,7 +60,11 @@ export {
|
|
|
47
60
|
} from "./meta.js"
|
|
48
61
|
|
|
49
62
|
export { buildLlmsTxt, buildLlmsFullTxt } from "./llms.js"
|
|
63
|
+
export { buildSecurityTxt } from "./security.js"
|
|
64
|
+
export { buildWebManifest } from "./manifest.js"
|
|
65
|
+
export { buildApiCatalog } from "./catalog.js"
|
|
50
66
|
export { buildArticleSchema, buildBreadcrumbSchema } from "./schema.js"
|
|
67
|
+
export { generatePng, generateMaskablePng, generateIco, generateOgCard } from "./assets.js"
|
|
51
68
|
export type { OgFontConfig, OgLayoutOptions, OgRenderConfig } from "./og.js"
|
|
52
69
|
|
|
53
70
|
import { rimelightSeo } from "./integration.js"
|