@raystack/chronicle 0.12.2 → 0.12.3
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/cli/index.js +1 -0
- package/package.json +1 -1
- package/src/fonts/Inter-Regular.ttf +0 -0
- package/src/pages/DocsPage.tsx +2 -1
- package/src/pages/LandingPage.tsx +8 -0
- package/src/server/App.tsx +19 -17
- package/src/server/entry-server.tsx +10 -0
- package/src/server/routes/og-utils.ts +32 -0
- package/src/server/routes/og.test.ts +63 -0
- package/src/server/routes/og.tsx +21 -19
- package/src/server/vite-config.ts +1 -0
- package/src/types/globals.d.ts +1 -0
package/dist/cli/index.js
CHANGED
|
@@ -496,6 +496,7 @@ async function createViteConfig(options) {
|
|
|
496
496
|
define: {
|
|
497
497
|
__CHRONICLE_CONTENT_DIR__: JSON.stringify(contentMirror),
|
|
498
498
|
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
|
|
499
|
+
__CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot),
|
|
499
500
|
__CHRONICLE_CONFIG_RAW__: JSON.stringify(rawConfig)
|
|
500
501
|
},
|
|
501
502
|
css: {
|
package/package.json
CHANGED
|
Binary file
|
package/src/pages/DocsPage.tsx
CHANGED
|
@@ -38,7 +38,8 @@ export function DocsPage({ slug }: DocsPageProps) {
|
|
|
38
38
|
'@type': 'Article',
|
|
39
39
|
headline: page.frontmatter.title,
|
|
40
40
|
description: page.frontmatter.description,
|
|
41
|
-
...(pageUrl && { url: pageUrl })
|
|
41
|
+
...(pageUrl && { url: pageUrl }),
|
|
42
|
+
...(page.frontmatter.lastModified && { dateModified: new Date(page.frontmatter.lastModified).toISOString() }),
|
|
42
43
|
}}
|
|
43
44
|
/>
|
|
44
45
|
<Page
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FolderIcon } from '@heroicons/react/24/outline';
|
|
2
2
|
import { Link as RouterLink } from 'react-router';
|
|
3
3
|
import { getLandingEntries } from '@/lib/config';
|
|
4
|
+
import { Head } from '@/lib/head';
|
|
4
5
|
import { usePageContext } from '@/lib/page-context';
|
|
5
6
|
import styles from './LandingPage.module.css';
|
|
6
7
|
|
|
@@ -13,6 +14,12 @@ export function LandingPage() {
|
|
|
13
14
|
: `${config.site.title} — ${versionLabel(config, version.dir)}`;
|
|
14
15
|
|
|
15
16
|
return (
|
|
17
|
+
<>
|
|
18
|
+
<Head
|
|
19
|
+
title={version.dir ? `${config.site.title} — ${versionLabel(config, version.dir)}` : 'Documentation'}
|
|
20
|
+
description={config.site.description}
|
|
21
|
+
config={config}
|
|
22
|
+
/>
|
|
16
23
|
<div className={styles.root}>
|
|
17
24
|
<div className={styles.header}>
|
|
18
25
|
<h1 className={styles.title}>{heading}</h1>
|
|
@@ -42,6 +49,7 @@ export function LandingPage() {
|
|
|
42
49
|
))}
|
|
43
50
|
</div>
|
|
44
51
|
</div>
|
|
52
|
+
</>
|
|
45
53
|
);
|
|
46
54
|
}
|
|
47
55
|
|
package/src/server/App.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import { ThemeProvider, Skeleton, Flex } from '@raystack/apsara';
|
|
|
4
4
|
import { lazy, Suspense } from 'react';
|
|
5
5
|
import { Navigate, useLocation } from 'react-router';
|
|
6
6
|
import { AnalyticsProvider } from '@/components/analytics/AnalyticsProvider';
|
|
7
|
-
import { Head } from '@/lib/head';
|
|
8
7
|
import { usePageContext } from '@/lib/page-context';
|
|
9
8
|
import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
10
9
|
import type { ChronicleConfig } from '@/types';
|
|
@@ -69,22 +68,25 @@ function PageFallback() {
|
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
function RootHead({ config }: { config: ChronicleConfig }) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
config.url
|
|
79
|
-
? {
|
|
80
|
-
'@context': 'https://schema.org',
|
|
81
|
-
'@type': 'WebSite',
|
|
82
|
-
name: config.site.title,
|
|
83
|
-
description: config.site.description,
|
|
84
|
-
url: config.url
|
|
85
|
-
}
|
|
86
|
-
: undefined
|
|
71
|
+
const siteJsonLd = config.url
|
|
72
|
+
? {
|
|
73
|
+
'@context': 'https://schema.org',
|
|
74
|
+
'@type': 'WebSite',
|
|
75
|
+
name: config.site.title,
|
|
76
|
+
description: config.site.description,
|
|
77
|
+
url: config.url,
|
|
87
78
|
}
|
|
88
|
-
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<title>{config.site.title}</title>
|
|
84
|
+
{siteJsonLd && (
|
|
85
|
+
<script
|
|
86
|
+
type='application/ld+json'
|
|
87
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(siteJsonLd, null, 2) }}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</>
|
|
89
91
|
);
|
|
90
92
|
}
|
|
@@ -158,6 +158,16 @@ export default {
|
|
|
158
158
|
const href = isLocalImage(src) && !isSvg(src) ? buildOptimizedUrl(src, DEFAULT_WIDTH) : src;
|
|
159
159
|
return <link key={src} rel="preload" as="image" href={href} />;
|
|
160
160
|
})}
|
|
161
|
+
{isApiRoute && (
|
|
162
|
+
<link
|
|
163
|
+
rel="preload"
|
|
164
|
+
as="fetch"
|
|
165
|
+
crossOrigin="anonymous"
|
|
166
|
+
href={route.version.dir
|
|
167
|
+
? `/api/specs?version=${encodeURIComponent(route.version.dir)}`
|
|
168
|
+
: '/api/specs'}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
161
171
|
<script type="module" src={assets.entry} />
|
|
162
172
|
<script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
|
|
163
173
|
</head>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const MIME_MAP: Record<string, string> = {
|
|
5
|
+
'.svg': 'image/svg+xml',
|
|
6
|
+
'.png': 'image/png',
|
|
7
|
+
'.jpg': 'image/jpeg',
|
|
8
|
+
'.jpeg': 'image/jpeg',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function getLogoDataUri(data: Buffer, filePath: string): string | null {
|
|
12
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
13
|
+
const mime = MIME_MAP[ext];
|
|
14
|
+
if (!mime) return null;
|
|
15
|
+
return `data:${mime};base64,${data.toString('base64')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function loadLogo(projectRoot: string, logoPath: string): Promise<string | null> {
|
|
19
|
+
try {
|
|
20
|
+
const filePath = path.resolve(projectRoot, 'public', logoPath.replace(/^\//, ''));
|
|
21
|
+
const data = await fs.readFile(filePath);
|
|
22
|
+
return getLogoDataUri(data, filePath);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loadFont(packageRoot: string): Promise<ArrayBuffer> {
|
|
29
|
+
const fontPath = path.resolve(packageRoot, 'src/fonts/Inter-Regular.ttf');
|
|
30
|
+
const buffer = await fs.readFile(fontPath);
|
|
31
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
32
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getLogoDataUri, loadLogo, loadFont } from './og-utils';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '../../..');
|
|
6
|
+
const FIXTURES = path.resolve(__dirname, '__fixtures__');
|
|
7
|
+
|
|
8
|
+
describe('getLogoDataUri', () => {
|
|
9
|
+
test('svg file returns svg mime type', () => {
|
|
10
|
+
const data = Buffer.from('<svg></svg>');
|
|
11
|
+
const result = getLogoDataUri(data, '/logo.svg');
|
|
12
|
+
expect(result).toStartWith('data:image/svg+xml;base64,');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('png file returns png mime type', () => {
|
|
16
|
+
const data = Buffer.from('fake-png');
|
|
17
|
+
const result = getLogoDataUri(data, '/logo.png');
|
|
18
|
+
expect(result).toStartWith('data:image/png;base64,');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('jpg file returns jpeg mime type', () => {
|
|
22
|
+
const data = Buffer.from('fake-jpg');
|
|
23
|
+
const result = getLogoDataUri(data, '/photo.jpg');
|
|
24
|
+
expect(result).toStartWith('data:image/jpeg;base64,');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('returns null for unsupported format', () => {
|
|
28
|
+
const data = Buffer.from('fake-webp');
|
|
29
|
+
const result = getLogoDataUri(data, '/logo.webp');
|
|
30
|
+
expect(result).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('encodes data as base64', () => {
|
|
34
|
+
const content = '<svg xmlns="http://www.w3.org/2000/svg"></svg>';
|
|
35
|
+
const data = Buffer.from(content);
|
|
36
|
+
const result = getLogoDataUri(data, '/icon.svg');
|
|
37
|
+
const base64 = result!.split(',')[1];
|
|
38
|
+
expect(Buffer.from(base64, 'base64').toString()).toBe(content);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('loadLogo', () => {
|
|
43
|
+
test('returns null for nonexistent file', async () => {
|
|
44
|
+
const result = await loadLogo('/nonexistent', '/logo.svg');
|
|
45
|
+
expect(result).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('strips leading slash from logo path', async () => {
|
|
49
|
+
const result = await loadLogo('/nonexistent', '/nested/logo.svg');
|
|
50
|
+
expect(result).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('loadFont', () => {
|
|
55
|
+
test('loads Inter font from package', async () => {
|
|
56
|
+
const font = await loadFont(PACKAGE_ROOT);
|
|
57
|
+
expect(font.byteLength).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('throws for invalid path', async () => {
|
|
61
|
+
expect(loadFont('/nonexistent')).rejects.toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/server/routes/og.tsx
CHANGED
|
@@ -2,23 +2,10 @@ import { defineHandler } from 'nitro';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import satori from 'satori';
|
|
4
4
|
import { loadConfig } from '@/lib/config';
|
|
5
|
+
import { loadFont, loadLogo } from './og-utils';
|
|
5
6
|
|
|
6
7
|
let fontData: ArrayBuffer | null = null;
|
|
7
|
-
|
|
8
|
-
async function loadFont(): Promise<ArrayBuffer> {
|
|
9
|
-
if (fontData) return fontData;
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
const response = await fetch(
|
|
13
|
-
'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2'
|
|
14
|
-
);
|
|
15
|
-
fontData = await response.arrayBuffer();
|
|
16
|
-
} catch {
|
|
17
|
-
fontData = new ArrayBuffer(0);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return fontData;
|
|
21
|
-
}
|
|
8
|
+
let cachedLogo: string | null | undefined;
|
|
22
9
|
|
|
23
10
|
export default defineHandler(async event => {
|
|
24
11
|
const config = loadConfig();
|
|
@@ -26,7 +13,12 @@ export default defineHandler(async event => {
|
|
|
26
13
|
const description = event.url.searchParams.get('description') ?? '';
|
|
27
14
|
const siteName = config.site.title;
|
|
28
15
|
|
|
29
|
-
|
|
16
|
+
if (!fontData) fontData = await loadFont(__CHRONICLE_PACKAGE_ROOT__);
|
|
17
|
+
if (cachedLogo === undefined) {
|
|
18
|
+
cachedLogo = config.logo?.dark
|
|
19
|
+
? await loadLogo(__CHRONICLE_PROJECT_ROOT__, config.logo.dark)
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
30
22
|
|
|
31
23
|
const svg = await satori(
|
|
32
24
|
<div
|
|
@@ -41,8 +33,18 @@ export default defineHandler(async event => {
|
|
|
41
33
|
color: '#fafafa',
|
|
42
34
|
}}
|
|
43
35
|
>
|
|
44
|
-
<div style={{
|
|
45
|
-
{
|
|
36
|
+
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 16 }}>
|
|
37
|
+
{cachedLogo && (
|
|
38
|
+
<img
|
|
39
|
+
src={cachedLogo}
|
|
40
|
+
width={48}
|
|
41
|
+
height={48}
|
|
42
|
+
style={{ marginRight: 16 }}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
<div style={{ fontSize: 32, color: '#888' }}>
|
|
46
|
+
{siteName}
|
|
47
|
+
</div>
|
|
46
48
|
</div>
|
|
47
49
|
<div
|
|
48
50
|
style={{
|
|
@@ -64,7 +66,7 @@ export default defineHandler(async event => {
|
|
|
64
66
|
width: 1200,
|
|
65
67
|
height: 630,
|
|
66
68
|
fonts: [
|
|
67
|
-
{ name: 'Inter', data:
|
|
69
|
+
{ name: 'Inter', data: fontData, weight: 400, style: 'normal' as const },
|
|
68
70
|
],
|
|
69
71
|
},
|
|
70
72
|
);
|
|
@@ -131,6 +131,7 @@ export async function createViteConfig(
|
|
|
131
131
|
define: {
|
|
132
132
|
__CHRONICLE_CONTENT_DIR__: JSON.stringify(contentMirror),
|
|
133
133
|
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
|
|
134
|
+
__CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot),
|
|
134
135
|
__CHRONICLE_CONFIG_RAW__: JSON.stringify(rawConfig),
|
|
135
136
|
},
|
|
136
137
|
css: {
|
package/src/types/globals.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Vite build-time constants (injected via define in vite-config.ts)
|
|
2
2
|
declare const __CHRONICLE_CONTENT_DIR__: string
|
|
3
3
|
declare const __CHRONICLE_PROJECT_ROOT__: string
|
|
4
|
+
declare const __CHRONICLE_PACKAGE_ROOT__: string
|
|
4
5
|
declare const __CHRONICLE_CONFIG_RAW__: string | null
|