Skip to content

Instantly share code, notes, and snippets.

@cassidoo
Created June 13, 2025 04:00
Show Gist options
  • Select an option

  • Save cassidoo/de9529ff2c5129b5e9a30a268c30b9ab to your computer and use it in GitHub Desktop.

Select an option

Save cassidoo/de9529ff2c5129b5e9a30a268c30b9ab to your computer and use it in GitHub Desktop.

Revisions

  1. cassidoo created this gist Jun 13, 2025.
    49 changes: 49 additions & 0 deletions og-generation.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,49 @@
    import puppeteer from "puppeteer";
    import fs from "fs/promises";
    import path from "path";
    import { fileURLToPath } from "url";
    import getPosts from "...";

    const __dirname = path.dirname(fileURLToPath(import.meta.url));

    async function generateOGImages() {
    const posts = await getPosts();
    const templatePath = path.join(__dirname, "../../dist/open-graph/index.html");

    const distDir = path.join(__dirname, "../../dist");

    console.log("Generating OG images...");
    console.log(`Found ${posts.length} slugs.`);

    const browser = await puppeteer.launch();

    // Set the limit when testing
    // let limit = 30;

    for (const post of posts) {
    // if (limit-- <= 0) {
    // console.log("Limit reached, stopping generation.");
    // break;
    // }

    const page = await browser.newPage();

    const url = `file://${templatePath}?title=${encodeURIComponent(post.title)}`;
    await page.goto(url);
    await page.setViewport({ width: 1200, height: 630 });

    const outputDir = path.join(distDir, "og-image");
    await fs.mkdir(outputDir, { recursive: true });

    await page.screenshot({
    path: path.join(outputDir, `${post.id}.png`),
    type: "png",
    });

    await page.close();
    }

    await browser.close();
    }

    generateOGImages().catch(console.error);
    123 changes: 123 additions & 0 deletions open-graph.astro
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,123 @@
    <div class="svg-container">
    <img src="../img/blank-card-opt.svg" class="svg-image" />

    <div class="svg-text-group">
    <div class="svg-overlay-text" id="title"></div>
    <div class="svg-overlay-text-sub">a blog post by cassidoo</div>
    </div>
    </div>

    <style>
    html,
    body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    }

    .svg-container {
    position: relative;
    margin: 0;
    padding: 0;
    display: inline-block;
    }

    .svg-image {
    display: block;
    width: 1200px;
    height: 630px;
    height: auto;
    }

    .svg-text-group {
    position: absolute;
    top: 140px;
    left: 60px;
    max-width: 960px;
    }

    .svg-overlay-text {
    font-family: "iA Writer Mono", monospace;
    font-size: clamp(3em, 3vw, 8em);
    text-wrap: balance;
    line-height: 1.2;
    background: #fff;
    padding: 20px 0 40px 20px;
    word-break: break-word;
    overflow-wrap: break-word;
    }

    .svg-overlay-text-sub {
    font-family: "iA Writer Mono", monospace;
    font-size: 3em;
    line-height: 1.5;
    color: #96979c;
    background: #fff;
    padding-left: 20px;
    margin-top: -5px;
    }
    </style>

    <script>
    function decodeHtmlEntities(text) {
    const textarea = document.createElement("textarea");
    textarea.innerHTML = text;
    return textarea.value;
    }

    const params = new URLSearchParams(window.location.search);
    const rawTitle = params.get("title") || "Untitled";
    const title = decodeHtmlEntities(rawTitle);
    document.getElementById("title").textContent = title;

    document.addEventListener("DOMContentLoaded", () => {
    const textGroup = document.querySelector(".svg-text-group");
    const overlayText = document.querySelector(".svg-overlay-text");
    const overlayTextSub = document.querySelector(".svg-overlay-text-sub");

    if (overlayText) {
    const minSize = 3;
    const maxSize = 7.5;
    const maxHeight = 400;

    let fontSize = maxSize;
    overlayText.style.fontSize = `${fontSize}em`;

    // Binary search for optimal font size
    let low = minSize;
    let high = maxSize;

    while (high - low > 0.1) {
    fontSize = (low + high) / 2;
    overlayText.style.fontSize = `${fontSize}em`;
    overlayText.offsetHeight;

    if (overlayText.scrollHeight > maxHeight) {
    high = fontSize;
    } else {
    low = fontSize;
    }
    }

    overlayText.style.fontSize = `${low}em`;

    // Calculate lines after final sizing
    const lineHeight = parseFloat(getComputedStyle(overlayText).lineHeight);
    const lines = Math.ceil(overlayText.offsetHeight / lineHeight);

    if (lines >= 3) {
    textGroup.style.top = "60px";
    }

    if (lines <= 2) {
    overlayTextSub.style.paddingBottom = "50px";
    }

    // Adjust height to be multiple of 100px
    const computedHeight = textGroup.offsetHeight;
    const roundedHeight = Math.ceil(computedHeight / 100) * 100;
    textGroup.style.height = `${roundedHeight}px`;
    }
    });
    </script>