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:
203
src/app/registers/[hex]/opengraph-image.tsx
Normal file
203
src/app/registers/[hex]/opengraph-image.tsx
Normal 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,
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user