assets: add approved portfolio evidence media
This commit is contained in:
74
scripts/generate-social-preview.mjs
Normal file
74
scripts/generate-social-preview.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdir, realpath } from 'node:fs/promises';
|
||||
import { dirname, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const permittedPrivateRoot = resolve(repositoryRoot, 'content/private');
|
||||
|
||||
const fail = () => {
|
||||
throw new Error('SOCIAL-PREVIEW-PATH-001');
|
||||
};
|
||||
|
||||
const canonicalizeForWrite = async (path) => {
|
||||
const absolute = resolve(path);
|
||||
let cursor = dirname(absolute);
|
||||
const suffix = [];
|
||||
while (true) {
|
||||
try {
|
||||
return resolve(await realpath(cursor), ...suffix, absolute.slice(dirname(absolute).length + 1));
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) fail();
|
||||
const parent = dirname(cursor);
|
||||
if (parent === cursor) fail();
|
||||
suffix.unshift(cursor.slice(parent.length + 1));
|
||||
cursor = parent;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isInside = (path, root) => path === root || path.startsWith(`${root}${sep}`);
|
||||
|
||||
export const generateSocialPreview = async (outputPath) => {
|
||||
if (typeof outputPath !== 'string' || outputPath.trim() === '') fail();
|
||||
const output = await canonicalizeForWrite(outputPath);
|
||||
const privateRoot = await canonicalizeForWrite(permittedPrivateRoot);
|
||||
if (!isInside(output, privateRoot) && !isInside(output, '/tmp')) fail();
|
||||
|
||||
const svg = Buffer.from(`
|
||||
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="630" fill="#070910"/>
|
||||
<rect y="356" width="1200" height="274" fill="#0b1019"/>
|
||||
<path d="M0 356H1200" stroke="#1b3246" stroke-width="2"/>
|
||||
<circle cx="880" cy="174" r="78" fill="#152638"/>
|
||||
<path d="M474 630L586 356H642L780 630Z" fill="#101b28"/>
|
||||
<path d="M544 630L606 356" stroke="#35c8d0" stroke-width="7" opacity="0.9"/>
|
||||
<path d="M701 630L623 356" stroke="#e3a44a" stroke-width="7" opacity="0.88"/>
|
||||
<path d="M602 566L613 522M614 490L619 458M620 430L623 405" stroke="#dfe9ef" stroke-width="5" stroke-linecap="round" opacity="0.72"/>
|
||||
<text x="92" y="183" font-family="DejaVu Sans, sans-serif" font-size="66" font-weight="700" fill="#f1f5f7">Luca Pîslaru</text>
|
||||
<text x="96" y="245" font-family="DejaVu Sans, sans-serif" font-size="29" font-weight="500" letter-spacing="0.6" fill="#9fc8d1">System Administrator & Full-Stack Developer</text>
|
||||
<path d="M96 286H342" stroke="#35c8d0" stroke-width="4"/>
|
||||
<path d="M350 286H430" stroke="#e3a44a" stroke-width="4"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await sharp(svg, { density: 72 })
|
||||
.flatten({ background: '#070910' })
|
||||
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
||||
.toFile(output);
|
||||
return output;
|
||||
};
|
||||
|
||||
const parseOutput = (argv) => {
|
||||
if (argv.length !== 2 || argv[0] !== '--output') fail();
|
||||
return argv[1];
|
||||
};
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
generateSocialPreview(parseOutput(process.argv.slice(2))).catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : 'SOCIAL-PREVIEW-001'}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user