Add OG images for register pages

- generate per-register metadata and OG thumbnails
- honor register text line breaks and de-duplicate summary lines

Signed-off-by: Codex@lucy.xalior.com
This commit is contained in:
2026-01-01 15:42:53 +00:00
parent 0a71b5f62e
commit 4467ef98fd

View File

@@ -0,0 +1,203 @@
import { ImageResponse } from 'next/og';
import { getRegisters } from '@/services/register.service';
export const runtime = 'nodejs';
export const size = {
width: 1200,
height: 630,
};
export const contentType = 'image/png';
const buildRegisterSummaryLines = (register: { description: string; text: string; modes: { text: string }[] }) => {
const isInfoLine = (line: string) =>
line.length > 0 &&
!line.startsWith('//') &&
!line.startsWith('(R') &&
!line.startsWith('(W') &&
!line.startsWith('(R/W') &&
!line.startsWith('*') &&
!/^bits?\s+\d/i.test(line);
const normalizeLines = (raw: string) => {
const lines: string[] = [];
const rawLines = raw.split('\n');
for (const rawLine of rawLines) {
const trimmed = rawLine.trim();
if (!trimmed) {
if (lines.length > 0 && lines[lines.length - 1] !== '') {
lines.push('');
}
continue;
}
if (isInfoLine(trimmed)) {
lines.push(trimmed);
}
}
return lines;
};
const textLines = normalizeLines(register.text);
const modeLines = register.modes.flatMap(mode => normalizeLines(mode.text));
const descriptionLines = normalizeLines(register.description);
const combined: string[] = [];
const appendBlock = (block: string[]) => {
if (block.length === 0) return;
if (combined.length > 0 && combined[combined.length - 1] !== '') {
combined.push('');
}
combined.push(...block);
};
appendBlock(textLines);
appendBlock(modeLines);
appendBlock(descriptionLines);
const deduped: string[] = [];
const seen = new Set<string>();
for (const line of combined) {
if (!line) {
if (deduped.length > 0 && deduped[deduped.length - 1] !== '') {
deduped.push('');
}
continue;
}
if (seen.has(line)) {
continue;
}
seen.add(line);
deduped.push(line);
}
return deduped.length > 0 ? deduped : ['Spectrum Next register details and bit-level behavior.'];
};
const splitLongWord = (word: string, maxLineLength: number) => {
if (word.length <= maxLineLength) return [word];
const chunks: string[] = [];
for (let idx = 0; idx < word.length; idx += maxLineLength - 1) {
chunks.push(`${word.slice(idx, idx + maxLineLength - 1)}-`);
}
const last = chunks[chunks.length - 1];
chunks[chunks.length - 1] = last.endsWith('-') ? last.slice(0, -1) : last;
return chunks;
};
const wrapText = (text: string, maxLineLength: number) => {
const words = text
.split(/\s+/)
.filter(Boolean)
.flatMap(word => splitLongWord(word, maxLineLength));
const lines: string[] = [];
let current = '';
for (const word of words) {
const next = current ? `${current} ${word}` : word;
if (next.length > maxLineLength && current) {
lines.push(current);
current = word;
} else {
current = next;
}
}
if (current) {
lines.push(current);
}
return lines;
};
const wrapTextLines = (sourceLines: string[], maxLineLength: number, maxLines: number) => {
const output: string[] = [];
for (const line of sourceLines) {
if (output.length >= maxLines) break;
if (!line) {
if (output.length > 0 && output[output.length - 1] !== '') {
output.push('');
}
continue;
}
const wrapped = wrapText(line, maxLineLength);
for (const wrappedLine of wrapped) {
if (output.length >= maxLines) break;
output.push(wrappedLine);
}
}
return output.slice(0, maxLines);
};
export default async function Image({ params }: { params: Promise<{ hex: string }> }) {
const { hex } = await params;
const targetHex = decodeURIComponent(hex).toLowerCase();
const registers = await getRegisters();
const register = registers.find(r => r.hex_address.toLowerCase() === targetHex);
const title = register ? `${register.hex_address} ${register.name}` : 'Spectrum Next Register';
const summaryLinesSource = register
? buildRegisterSummaryLines(register)
: ['Register details not found.'];
const decAddress = register ? `Dec ${register.dec_address}` : '';
const titleLines = wrapTextLines([title], 32, 2);
const summaryLines = wrapTextLines(summaryLinesSource, 54, 6);
return new ImageResponse(
(
<div
style={{
width: '1200px',
height: '630px',
display: 'flex',
background: 'linear-gradient(135deg, #3f1f6e 0%, #593196 55%, #7a4cc4 100%)',
color: '#f3f2ed',
fontFamily: 'Arial, sans-serif',
padding: '64px',
boxSizing: 'border-box',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: '100%' }}>
<div style={{ fontSize: '28px', letterSpacing: '2px', textTransform: 'uppercase', color: '#e8dcff' }}>
Spectrum Next Explorer
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '6px',
fontSize: '68px',
fontWeight: 700,
lineHeight: 1.05,
}}
>
{titleLines.map(line => (
<div key={line}>{line}</div>
))}
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '6px',
fontSize: '32px',
lineHeight: 1.35,
color: '#f7f1ff',
}}
>
{summaryLines.map(line => (
<div key={line}>{line}</div>
))}
</div>
<div style={{ marginTop: 'auto', fontSize: '26px', color: '#dacbff' }}>
{decAddress}
</div>
</div>
</div>
),
{
width: size.width,
height: size.height,
}
);
}