-
Notifications
You must be signed in to change notification settings - Fork 1
fix: use local TTF font and render logo in OG image #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba6d3b1
af39eac
82a0867
98c155a
c2fa006
e7c3873
ba1c7a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ import { ThemeProvider, Skeleton, Flex } from '@raystack/apsara'; | |
| import { lazy, Suspense } from 'react'; | ||
| import { Navigate, useLocation } from 'react-router'; | ||
| import { AnalyticsProvider } from '@/components/analytics/AnalyticsProvider'; | ||
| import { Head } from '@/lib/head'; | ||
| import { usePageContext } from '@/lib/page-context'; | ||
| import { resolveRoute, RouteType } from '@/lib/route-resolver'; | ||
| import type { ChronicleConfig } from '@/types'; | ||
|
|
@@ -69,22 +68,25 @@ function PageFallback() { | |
| } | ||
|
|
||
| function RootHead({ config }: { config: ChronicleConfig }) { | ||
| return ( | ||
| <Head | ||
| title={config.site.title} | ||
| description={config.site.description} | ||
| config={config} | ||
| jsonLd={ | ||
| config.url | ||
| ? { | ||
| '@context': 'https://schema.org', | ||
| '@type': 'WebSite', | ||
| name: config.site.title, | ||
| description: config.site.description, | ||
| url: config.url | ||
| } | ||
| : undefined | ||
| const siteJsonLd = config.url | ||
| ? { | ||
| '@context': 'https://schema.org', | ||
| '@type': 'WebSite', | ||
| name: config.site.title, | ||
| description: config.site.description, | ||
| url: config.url, | ||
| } | ||
| /> | ||
| : null; | ||
|
|
||
| return ( | ||
| <> | ||
| <title>{config.site.title}</title> | ||
| {siteJsonLd && ( | ||
| <script | ||
| type='application/ld+json' | ||
| dangerouslySetInnerHTML={{ __html: JSON.stringify(siteJsonLd, null, 2) }} | ||
|
Comment on lines
+85
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
python - <<'PY'
import json
payload = {"name": "</script><script>alert(1)</script>"}
rendered = json.dumps(payload, indent=2)
print(rendered)
assert "</script>" in rendered, "Expected raw closing script tag in JSON.stringify-equivalent output"
PY
rg -n -C2 "dangerouslySetInnerHTML=\{\{ __html: JSON.stringify" packages/chronicle/srcRepository: raystack/chronicle Length of output: 826 Security: escape JSON-LD output before injecting into the
🔒 Suggested fix+function serializeJsonLd(value: unknown) {
+ return JSON.stringify(value, null, 2)
+ .replace(/</g, '\\u003c')
+ .replace(/\u2028/g, '\\u2028')
+ .replace(/\u2029/g, '\\u2029');
+}
+
function RootHead({ config }: { config: ChronicleConfig }) {
const siteJsonLd = config.url
? {
'`@context`': 'https://schema.org',
'`@type`': 'WebSite',
name: config.site.title,
description: config.site.description,
url: config.url,
}
: null;
return (
<>
<title>{config.site.title}</title>
{siteJsonLd && (
<script
type='application/ld+json'
- dangerouslySetInnerHTML={{ __html: JSON.stringify(siteJsonLd, null, 2) }}
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(siteJsonLd) }}
/>
)}
</>
);
}Apply the same 🧰 Tools🪛 ast-grep (0.43.0)[warning] 86-86: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks. (react-unsafe-html-injection) 🤖 Prompt for AI Agents |
||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import fs from 'node:fs/promises'; | ||
| import path from 'node:path'; | ||
|
|
||
| const MIME_MAP: Record<string, string> = { | ||
| '.svg': 'image/svg+xml', | ||
| '.png': 'image/png', | ||
| '.jpg': 'image/jpeg', | ||
| '.jpeg': 'image/jpeg', | ||
| }; | ||
|
|
||
| export function getLogoDataUri(data: Buffer, filePath: string): string | null { | ||
| const ext = path.extname(filePath).toLowerCase(); | ||
| const mime = MIME_MAP[ext]; | ||
| if (!mime) return null; | ||
| return `data:${mime};base64,${data.toString('base64')}`; | ||
| } | ||
|
|
||
| export async function loadLogo(projectRoot: string, logoPath: string): Promise<string | null> { | ||
| try { | ||
| const filePath = path.resolve(projectRoot, 'public', logoPath.replace(/^\//, '')); | ||
| const data = await fs.readFile(filePath); | ||
| return getLogoDataUri(data, filePath); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export async function loadFont(packageRoot: string): Promise<ArrayBuffer> { | ||
| const fontPath = path.resolve(packageRoot, 'src/fonts/Inter-Regular.ttf'); | ||
| const buffer = await fs.readFile(fontPath); | ||
| return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { describe, expect, test } from 'bun:test'; | ||
| import path from 'node:path'; | ||
| import { getLogoDataUri, loadLogo, loadFont } from './og-utils'; | ||
|
|
||
| const PACKAGE_ROOT = path.resolve(__dirname, '../../..'); | ||
| const FIXTURES = path.resolve(__dirname, '__fixtures__'); | ||
|
|
||
| describe('getLogoDataUri', () => { | ||
| test('svg file returns svg mime type', () => { | ||
| const data = Buffer.from('<svg></svg>'); | ||
| const result = getLogoDataUri(data, '/logo.svg'); | ||
| expect(result).toStartWith('data:image/svg+xml;base64,'); | ||
| }); | ||
|
|
||
| test('png file returns png mime type', () => { | ||
| const data = Buffer.from('fake-png'); | ||
| const result = getLogoDataUri(data, '/logo.png'); | ||
| expect(result).toStartWith('data:image/png;base64,'); | ||
| }); | ||
|
|
||
| test('jpg file returns jpeg mime type', () => { | ||
| const data = Buffer.from('fake-jpg'); | ||
| const result = getLogoDataUri(data, '/photo.jpg'); | ||
| expect(result).toStartWith('data:image/jpeg;base64,'); | ||
| }); | ||
|
|
||
| test('returns null for unsupported format', () => { | ||
| const data = Buffer.from('fake-webp'); | ||
| const result = getLogoDataUri(data, '/logo.webp'); | ||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| test('encodes data as base64', () => { | ||
| const content = '<svg xmlns="http://www.w3.org/2000/svg"></svg>'; | ||
| const data = Buffer.from(content); | ||
| const result = getLogoDataUri(data, '/icon.svg'); | ||
| const base64 = result!.split(',')[1]; | ||
| expect(Buffer.from(base64, 'base64').toString()).toBe(content); | ||
| }); | ||
| }); | ||
|
|
||
| describe('loadLogo', () => { | ||
| test('returns null for nonexistent file', async () => { | ||
| const result = await loadLogo('/nonexistent', '/logo.svg'); | ||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| test('strips leading slash from logo path', async () => { | ||
| const result = await loadLogo('/nonexistent', '/nested/logo.svg'); | ||
| expect(result).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('loadFont', () => { | ||
| test('loads Inter font from package', async () => { | ||
| const font = await loadFont(PACKAGE_ROOT); | ||
| expect(font.byteLength).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| test('throws for invalid path', async () => { | ||
| expect(loadFont('/nonexistent')).rejects.toThrow(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| // Vite build-time constants (injected via define in vite-config.ts) | ||
| declare const __CHRONICLE_CONTENT_DIR__: string | ||
| declare const __CHRONICLE_PROJECT_ROOT__: string | ||
| declare const __CHRONICLE_PACKAGE_ROOT__: string | ||
| declare const __CHRONICLE_CONFIG_RAW__: string | null |
Uh oh!
There was an error while loading. Please reload this page.