@amjadiqbal/start-seo-kit 0.0.0-stage → 0.1.1
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/CHANGELOG.md +37 -0
- package/LICENSE.md +21 -0
- package/README.md +338 -2
- package/dist/commerce.d.ts +151 -0
- package/dist/commerce.js +315 -0
- package/dist/head-f4tZr5jl.d.ts +218 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +429 -0
- package/dist/json-ld-BkYFVGHw.js +122 -0
- package/dist/og.d.ts +64 -0
- package/dist/og.js +142 -0
- package/dist/sitemap-BiQmwT37.d.ts +99 -0
- package/package.json +89 -4
package/dist/og.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/og.ts
|
|
2
|
+
let resvgReady;
|
|
3
|
+
async function loadResvg(wasm) {
|
|
4
|
+
resvgReady ??= (async () => {
|
|
5
|
+
const resvg = await import("@resvg/resvg-wasm");
|
|
6
|
+
const source = typeof wasm === "function" ? await wasm() : wasm ?? await nodeWasm();
|
|
7
|
+
await resvg.initWasm(source);
|
|
8
|
+
return resvg;
|
|
9
|
+
})().catch((error) => {
|
|
10
|
+
resvgReady = void 0;
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
return resvgReady;
|
|
14
|
+
}
|
|
15
|
+
async function nodeWasm() {
|
|
16
|
+
try {
|
|
17
|
+
const { readFile } = await import("node:fs/promises");
|
|
18
|
+
const { createRequire } = await import("node:module");
|
|
19
|
+
return await readFile(createRequire(import.meta.url).resolve("@resvg/resvg-wasm/index_bg.wasm"));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error("start-seo-kit/og: could not load @resvg/resvg-wasm/index_bg.wasm. Install @resvg/resvg-wasm, or pass the wasm option, or use format: \"svg\".", { cause: error });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Render an element to PNG bytes (or an SVG string with `format: 'svg'`). */
|
|
25
|
+
async function renderOgImage(element, options) {
|
|
26
|
+
const width = options.width ?? 1200;
|
|
27
|
+
const height = options.height ?? 630;
|
|
28
|
+
const { default: satori } = await import("satori");
|
|
29
|
+
const svg = await satori(element, {
|
|
30
|
+
width,
|
|
31
|
+
height,
|
|
32
|
+
fonts: options.fonts.map((f) => ({
|
|
33
|
+
...f,
|
|
34
|
+
data: toArrayBuffer(f.data)
|
|
35
|
+
}))
|
|
36
|
+
});
|
|
37
|
+
if (options.format === "svg") return svg;
|
|
38
|
+
const { Resvg } = await loadResvg(options.wasm);
|
|
39
|
+
const rendered = new Resvg(svg, { fitTo: {
|
|
40
|
+
mode: "width",
|
|
41
|
+
value: width
|
|
42
|
+
} }).render();
|
|
43
|
+
try {
|
|
44
|
+
return rendered.asPng();
|
|
45
|
+
} finally {
|
|
46
|
+
rendered.free();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `GET` handler for an Open Graph image server route, e.g. `src/routes/og/$slug.ts`.
|
|
51
|
+
* Point `seo({ image: '/og/my-post' })` at it.
|
|
52
|
+
*/
|
|
53
|
+
function createOgImageHandler(options) {
|
|
54
|
+
let fonts;
|
|
55
|
+
return async (ctx) => {
|
|
56
|
+
fonts ??= Promise.resolve(typeof options.fonts === "function" ? options.fonts() : options.fonts).catch((error) => {
|
|
57
|
+
fonts = void 0;
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
60
|
+
const element = await options.render({
|
|
61
|
+
...ctx,
|
|
62
|
+
url: new URL(ctx.request.url)
|
|
63
|
+
});
|
|
64
|
+
if (element === null) return new Response("Not Found", { status: 404 });
|
|
65
|
+
const body = await renderOgImage(element, {
|
|
66
|
+
...options,
|
|
67
|
+
fonts: await fonts
|
|
68
|
+
});
|
|
69
|
+
return new Response(typeof body === "string" ? body : body, { headers: {
|
|
70
|
+
"Content-Type": typeof body === "string" ? "image/svg+xml" : "image/png",
|
|
71
|
+
"Cache-Control": options.cacheControl ?? "public, max-age=86400, immutable"
|
|
72
|
+
} });
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** A ready-made 1200x630 card (title, description, label, footer) as a plain element tree; no JSX needed. */
|
|
76
|
+
function ogCard(input) {
|
|
77
|
+
const fg = input.foreground ?? "#F8FAFC";
|
|
78
|
+
const muted = input.muted ?? "#94A3B8";
|
|
79
|
+
const accent = input.accent ?? "#34D399";
|
|
80
|
+
const el = (style, children) => ({
|
|
81
|
+
type: "div",
|
|
82
|
+
props: {
|
|
83
|
+
style,
|
|
84
|
+
children
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const titleSize = input.title.length > 60 ? 56 : input.title.length > 34 ? 68 : 80;
|
|
88
|
+
return el({
|
|
89
|
+
width: "100%",
|
|
90
|
+
height: "100%",
|
|
91
|
+
display: "flex",
|
|
92
|
+
flexDirection: "column",
|
|
93
|
+
background: input.background ?? "#0B1220",
|
|
94
|
+
color: fg,
|
|
95
|
+
fontFamily: input.fontFamily
|
|
96
|
+
}, [el({
|
|
97
|
+
display: "flex",
|
|
98
|
+
flexDirection: "column",
|
|
99
|
+
justifyContent: "space-between",
|
|
100
|
+
flexGrow: 1,
|
|
101
|
+
margin: "72px 80px"
|
|
102
|
+
}, [el({
|
|
103
|
+
display: "flex",
|
|
104
|
+
flexDirection: "column",
|
|
105
|
+
gap: 24
|
|
106
|
+
}, [
|
|
107
|
+
el({
|
|
108
|
+
display: "flex",
|
|
109
|
+
width: 96,
|
|
110
|
+
height: 8,
|
|
111
|
+
borderRadius: 4,
|
|
112
|
+
background: accent
|
|
113
|
+
}, []),
|
|
114
|
+
input.eyebrow ? el({
|
|
115
|
+
display: "flex",
|
|
116
|
+
fontSize: 30,
|
|
117
|
+
color: accent,
|
|
118
|
+
fontWeight: 600
|
|
119
|
+
}, input.eyebrow) : null,
|
|
120
|
+
el({
|
|
121
|
+
display: "flex",
|
|
122
|
+
fontSize: titleSize,
|
|
123
|
+
fontWeight: 700,
|
|
124
|
+
lineHeight: 1.1
|
|
125
|
+
}, input.title),
|
|
126
|
+
input.description ? el({
|
|
127
|
+
display: "flex",
|
|
128
|
+
fontSize: 32,
|
|
129
|
+
color: muted,
|
|
130
|
+
lineHeight: 1.35
|
|
131
|
+
}, input.description) : null
|
|
132
|
+
].filter(Boolean)), input.footer ? el({
|
|
133
|
+
display: "flex",
|
|
134
|
+
fontSize: 26,
|
|
135
|
+
color: muted
|
|
136
|
+
}, input.footer) : el({ display: "flex" }, [])])]);
|
|
137
|
+
}
|
|
138
|
+
function toArrayBuffer(data) {
|
|
139
|
+
if (data instanceof ArrayBuffer) return data;
|
|
140
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
141
|
+
}
|
|
142
|
+
export { createOgImageHandler, ogCard, renderOgImage };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
type TrailingSlash = 'never' | 'always';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a path or URL against the site. Absolute http(s) URLs are returned unchanged.
|
|
4
|
+
* Pass `trailingSlash` for page URLs only; asset URLs (images) keep their path as given.
|
|
5
|
+
*/
|
|
6
|
+
declare function absoluteUrl(siteUrl: string, pathOrUrl: string, trailingSlash?: TrailingSlash): string;
|
|
7
|
+
type Awaitable<T> = T | Promise<T>;
|
|
8
|
+
/** The context a TanStack Start server route handler receives (only the parts used here). */
|
|
9
|
+
interface HandlerContext {
|
|
10
|
+
request: Request;
|
|
11
|
+
params?: Record<string, string | undefined> | object;
|
|
12
|
+
}
|
|
13
|
+
type ServerHandler = (ctx: HandlerContext) => Promise<Response>;
|
|
14
|
+
type ChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
15
|
+
interface SitemapImage {
|
|
16
|
+
url: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
caption?: string;
|
|
19
|
+
}
|
|
20
|
+
interface SitemapEntry {
|
|
21
|
+
/** Page path (`/posts/hello`) or absolute URL. */
|
|
22
|
+
loc: string;
|
|
23
|
+
lastmod?: string | Date;
|
|
24
|
+
changefreq?: ChangeFreq;
|
|
25
|
+
/** 0.0 to 1.0. */
|
|
26
|
+
priority?: number;
|
|
27
|
+
/** hreflang alternates, e.g. `{ en: '/en/about', de: '/de/about', 'x-default': '/about' }`. */
|
|
28
|
+
alternates?: Record<string, string>;
|
|
29
|
+
images?: Array<string | SitemapImage>;
|
|
30
|
+
}
|
|
31
|
+
interface UrlOptions {
|
|
32
|
+
siteUrl: string;
|
|
33
|
+
/** Match your router's `trailingSlash` option. Default `never`. */
|
|
34
|
+
trailingSlash?: TrailingSlash;
|
|
35
|
+
}
|
|
36
|
+
/** Render a `<urlset>` sitemap. */
|
|
37
|
+
declare function sitemapXml(entries: Iterable<SitemapEntry>, options: UrlOptions): string;
|
|
38
|
+
interface SitemapIndexEntry {
|
|
39
|
+
/** Path or absolute URL of a child sitemap. */
|
|
40
|
+
loc: string;
|
|
41
|
+
lastmod?: string | Date;
|
|
42
|
+
}
|
|
43
|
+
/** Render a `<sitemapindex>`. */
|
|
44
|
+
declare function sitemapIndexXml(sitemaps: Iterable<SitemapIndexEntry>, options: UrlOptions): string;
|
|
45
|
+
/**
|
|
46
|
+
* Where the URLs of one sitemap section come from:
|
|
47
|
+
* - an array of entries,
|
|
48
|
+
* - a function returning them (sync or async, e.g. a database query), called once per request
|
|
49
|
+
* to `/sitemap.xml` and once per child-sitemap request, loading every entry each time, or
|
|
50
|
+
* - a paginated source with `count()` and `entries({ offset, limit })` for large sets:
|
|
51
|
+
* the index calls only `count()`, and each child sitemap loads only its own range.
|
|
52
|
+
*/
|
|
53
|
+
type SitemapSource = SitemapEntry[] | (() => Awaitable<SitemapEntry[]>) | {
|
|
54
|
+
count: () => Awaitable<number>;
|
|
55
|
+
entries: (range: {
|
|
56
|
+
offset: number;
|
|
57
|
+
limit: number;
|
|
58
|
+
}) => Awaitable<SitemapEntry[]>;
|
|
59
|
+
lastmod?: () => Awaitable<string | Date | undefined>;
|
|
60
|
+
};
|
|
61
|
+
interface CreateSitemapOptions extends UrlOptions {
|
|
62
|
+
/** Named sections, e.g. `{ pages: routesFromTree(routeTree), posts: () => db.posts.list() }`. Names: letters, digits, `_`, `-`. */
|
|
63
|
+
sources: Record<string, SitemapSource>;
|
|
64
|
+
/** URLs per child sitemap. Default 50,000 (the protocol limit). */
|
|
65
|
+
maxUrlsPerSitemap?: number;
|
|
66
|
+
/**
|
|
67
|
+
* `auto` (default): `/sitemap.xml` is a plain urlset while everything fits in one file, and a sitemap index otherwise.
|
|
68
|
+
* `true`: always an index. `false`: always one urlset (throws past `maxUrlsPerSitemap`).
|
|
69
|
+
*/
|
|
70
|
+
index?: 'auto' | boolean;
|
|
71
|
+
/** Path the child sitemaps are served under. Default `/sitemaps` (route file `src/routes/sitemaps/$name.ts`). */
|
|
72
|
+
basePath?: string;
|
|
73
|
+
/** `Cache-Control` header. Default `public, max-age=3600`. */
|
|
74
|
+
cacheControl?: string;
|
|
75
|
+
}
|
|
76
|
+
interface SitemapHandlers {
|
|
77
|
+
/** `GET` handler for `/sitemap.xml`. */
|
|
78
|
+
index: ServerHandler;
|
|
79
|
+
/** `GET` handler for `/sitemaps/$name` (child sitemaps such as `/sitemaps/posts-1.xml`). */
|
|
80
|
+
page: ServerHandler;
|
|
81
|
+
/** Every entry from every source (useful for tests and prerendering). */
|
|
82
|
+
entries(): Promise<SitemapEntry[]>;
|
|
83
|
+
}
|
|
84
|
+
/** Dynamic sitemap (and sitemap index for large sites) served from TanStack Start server routes. */
|
|
85
|
+
declare function createSitemap(options: CreateSitemapOptions): SitemapHandlers;
|
|
86
|
+
interface RoutesFromTreeOptions {
|
|
87
|
+
/** Paths to leave out: exact strings, or RegExps tested against the path. */
|
|
88
|
+
exclude?: Array<string | RegExp>;
|
|
89
|
+
/** Defaults for every entry (e.g. `{ changefreq: 'weekly' }`). */
|
|
90
|
+
defaults?: Partial<Omit<SitemapEntry, 'loc'>>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Collect the static (parameter-free) page paths of your TanStack Router route tree.
|
|
94
|
+
* Dynamic routes (`$id`, `$`) are skipped: list them from your data in another source.
|
|
95
|
+
* Skipped too: server-only routes, paths with a file extension (`/sitemap.xml`), and routes with
|
|
96
|
+
* `staticData: { sitemap: false }`. Per-route `staticData.sitemap` values (priority, changefreq) are applied.
|
|
97
|
+
*/
|
|
98
|
+
declare function routesFromTree(routeTree: unknown, options?: RoutesFromTreeOptions): SitemapEntry[];
|
|
99
|
+
export { absoluteUrl as _, ServerHandler as a, SitemapImage as c, UrlOptions as d, createSitemap as f, TrailingSlash as g, sitemapXml as h, RoutesFromTreeOptions as i, SitemapIndexEntry as l, sitemapIndexXml as m, CreateSitemapOptions as n, SitemapEntry as o, routesFromTree as p, HandlerContext as r, SitemapHandlers as s, ChangeFreq as t, SitemapSource as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amjadiqbal/start-seo-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "SEO kit for TanStack Start: typed head() meta tags (title templates, canonical, Open Graph, Twitter cards, robots, hreflang), dynamic sitemap and robots.txt server routes, JSON-LD helpers, e-commerce Product schema and an optional Open Graph image route.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tanstack",
|
|
7
|
+
"tanstack-start",
|
|
8
|
+
"tanstack-router",
|
|
9
|
+
"seo",
|
|
10
|
+
"meta-tags",
|
|
11
|
+
"sitemap",
|
|
12
|
+
"sitemap-index",
|
|
13
|
+
"robots-txt",
|
|
14
|
+
"json-ld",
|
|
15
|
+
"structured-data",
|
|
16
|
+
"ecommerce",
|
|
17
|
+
"product-schema",
|
|
18
|
+
"rich-results",
|
|
19
|
+
"open-graph",
|
|
20
|
+
"og-image",
|
|
21
|
+
"twitter-card",
|
|
22
|
+
"hreflang"
|
|
23
|
+
],
|
|
24
|
+
"homepage": "https://github.com/amjadiqbal/start-seo-kit#readme",
|
|
25
|
+
"bugs": "https://github.com/amjadiqbal/start-seo-kit/issues",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/amjadiqbal/start-seo-kit.git"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"author": "Amjad Iqbal (https://amjad.com.pk)",
|
|
32
|
+
"type": "module",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"default": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./commerce": {
|
|
40
|
+
"types": "./dist/commerce.d.ts",
|
|
41
|
+
"default": "./dist/commerce.js"
|
|
42
|
+
},
|
|
43
|
+
"./og": {
|
|
44
|
+
"types": "./dist/og.d.ts",
|
|
45
|
+
"default": "./dist/og.js"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"CHANGELOG.md"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsdown",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"lint": "oxlint src tests",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"ci": "bash scripts/ci-local.sh"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@resvg/resvg-wasm": "^2.6.0",
|
|
65
|
+
"satori": ">=0.10.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@resvg/resvg-wasm": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"satori": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@fontsource/inter": "^5.3.0",
|
|
77
|
+
"@resvg/resvg-wasm": "^2.6.2",
|
|
78
|
+
"@tanstack/react-router": "^1.170.45",
|
|
79
|
+
"@tanstack/react-start": "^1.169.3",
|
|
80
|
+
"@types/node": "^22.20.5",
|
|
81
|
+
"@types/react": "^19.3.0",
|
|
82
|
+
"oxlint": "^1.89.0",
|
|
83
|
+
"publint": "^0.3.25",
|
|
84
|
+
"react": "^19.3.1",
|
|
85
|
+
"react-dom": "^19.3.1",
|
|
86
|
+
"satori": "^0.63.2",
|
|
87
|
+
"tsdown": "^0.23.1",
|
|
88
|
+
"typescript": "^5.9.3",
|
|
89
|
+
"vitest": "^5.0.5"
|
|
90
|
+
}
|
|
91
|
+
}
|