jamdesk 1.1.217 → 1.1.218
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/package.json +1 -1
- package/vendored/app/api/og/route.tsx +4 -2
- package/vendored/lib/og-card.ts +25 -0
- package/vendored/lib/seo.ts +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.218",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ImageResponse } from '@vercel/og';
|
|
2
2
|
import { NextRequest } from 'next/server';
|
|
3
3
|
import { sanitizeLogoUrl } from '@/lib/sanitize-url';
|
|
4
|
-
import { parseCardOptions } from '@/lib/og-card';
|
|
4
|
+
import { parseCardOptions, isRenderableImageType } from '@/lib/og-card';
|
|
5
5
|
|
|
6
6
|
export const runtime = 'edge';
|
|
7
7
|
|
|
@@ -18,7 +18,7 @@ function truncateText(text: string, maxLength: number): string {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Fetch an image and convert it to a base64 data URI for Satori compatibility
|
|
21
|
-
* (Satori
|
|
21
|
+
* (Satori can fail on remote URLs). 3s timeout; '' on failure or on a type Satori can't decode.
|
|
22
22
|
*/
|
|
23
23
|
async function fetchDataUri(url: string): Promise<string> {
|
|
24
24
|
const controller = new AbortController();
|
|
@@ -28,6 +28,8 @@ async function fetchDataUri(url: string): Promise<string> {
|
|
|
28
28
|
const res = await fetch(url, { signal: controller.signal, redirect: 'manual' });
|
|
29
29
|
if (!res.ok) return '';
|
|
30
30
|
const contentType = res.headers.get('content-type') || 'image/png';
|
|
31
|
+
// Satori throws on these and the route would answer 200 with an empty body; "no image" is the safe outcome.
|
|
32
|
+
if (!isRenderableImageType(contentType)) return '';
|
|
31
33
|
const buf = await res.arrayBuffer();
|
|
32
34
|
return `data:${contentType};base64,${Buffer.from(buf).toString('base64')}`;
|
|
33
35
|
} catch {
|
package/vendored/lib/og-card.ts
CHANGED
|
@@ -117,3 +117,28 @@ export function parseCardOptions(params: URLSearchParams): CardOptions {
|
|
|
117
117
|
showSiteName: params.get('hideName') !== '1',
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Satori throws on these, and the route then answers 200 with an empty body that
|
|
123
|
+
* the CDN caches as a blank card. A deny-list rather than an allow-list on purpose:
|
|
124
|
+
* any type not listed renders (or fails) exactly as it does today, so no root tenant
|
|
125
|
+
* loses a logo that currently shows. ICO is deliberately NOT listed: Satori drops the
|
|
126
|
+
* icon but still renders a full card (with the logo slot reserved), and an existing
|
|
127
|
+
* card may only change when it is broken. Measured 2026-10-02; GIF, SVG, JPEG, PNG and
|
|
128
|
+
* octet-stream all draw.
|
|
129
|
+
*/
|
|
130
|
+
const UNRENDERABLE_IMAGE_TYPES = new Set([
|
|
131
|
+
'image/webp', 'image/avif',
|
|
132
|
+
'image/tiff', 'image/heic', 'image/heif', 'image/jxl', 'image/bmp',
|
|
133
|
+
// Non-standard spellings of JPEG that real servers send; Satori only knows image/jpeg.
|
|
134
|
+
'image/jpg', 'image/pjpeg',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
export function isRenderableImageType(contentType: string): boolean {
|
|
138
|
+
const type = contentType.split(';')[0].trim().toLowerCase();
|
|
139
|
+
// A 200 HTML page for a missing image (soft 404) is the realistic non-image case.
|
|
140
|
+
// Only plain-letter subtypes throw (html, plain, xml, css, svg+xml); text/x-generic,
|
|
141
|
+
// text/vnd.foo and text/html5 are byte-sniffed and draw today, so they must stay allowed.
|
|
142
|
+
if (/^text\/[a-z+]+$/.test(type)) return false;
|
|
143
|
+
return !UNRENDERABLE_IMAGE_TYPES.has(type);
|
|
144
|
+
}
|
package/vendored/lib/seo.ts
CHANGED
|
@@ -96,6 +96,15 @@ function isUndecodableImage(pathOrUrl: string): boolean {
|
|
|
96
96
|
return /\.(?:webp|avif)$/i.test(pathOrUrl.split(/[?#]/)[0]);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* True for formats the OG card reliably draws as a logo. Deliberately an
|
|
101
|
+
* allow-list: ICO renders as an empty slot and WebP/AVIF are dropped, so none
|
|
102
|
+
* of them gain anything from the subpath /_jd/ rewrite.
|
|
103
|
+
*/
|
|
104
|
+
function isSubpathRewritableImage(pathOrUrl: string): boolean {
|
|
105
|
+
return /\.(?:png|jpe?g|gif|svg)$/i.test(pathOrUrl.split(/[?#]/)[0]);
|
|
106
|
+
}
|
|
107
|
+
|
|
99
108
|
/**
|
|
100
109
|
* Resolve the tenant's effective docs prefix and the domain-root URL that
|
|
101
110
|
* OG assets and metadataBase resolve against.
|
|
@@ -169,7 +178,13 @@ function buildOgImageUrl(
|
|
|
169
178
|
}
|
|
170
179
|
// Make logo URL absolute (use root URL since assets are at domain root)
|
|
171
180
|
if (logoPath && !logoPath.startsWith('http')) {
|
|
172
|
-
|
|
181
|
+
// A subpath tenant's worker only proxies /docs and /_jd/, so a bare
|
|
182
|
+
// root-relative logo 404s on their origin; /_jd/ is where it is served.
|
|
183
|
+
// Gated to formats the card renders, so ICO/WebP/AVIF keep today's URL
|
|
184
|
+
// rather than turning "no logo" into an empty icon slot.
|
|
185
|
+
logoPath = isHostAtDocs && isSubpathRewritableImage(logoPath)
|
|
186
|
+
? toAbsoluteAssetUrl(logoPath, rootUrl)
|
|
187
|
+
: `${rootUrl}${logoPath.startsWith('/') ? '' : '/'}${logoPath}`;
|
|
173
188
|
}
|
|
174
189
|
if (logoPath) {
|
|
175
190
|
params.set('logo', logoPath);
|