Robert Birming

Bear OG image generator

No, not original gangster images. If you haven't heard of OG (Open Graph) images, you've definitely seen them. It's those cards that are displayed when sharing posts on social media and messaging apps.

This dashboard plugin for Bear Blog makes it quick and easy to generate nice looking Open Graph images for your Bear blog posts.

Preview

Bear OG image generator

How to use

After installation, you'll see a new 'OG image' option in the editor when writing a post. It grabs the title and domain name, and you can add a tagline and an emoji or custom symbol.

When you're happy with the result, simply click the 'Download PNG' button. Upload the image to your Bear media library using the 'Upload raw' option. Copy the URL and add it to your post together with the meta_image attribute. Done!

Installation

The plugin requires JavaScript, available on Bear Blog's premium plan.

  1. Copy the script and styles below.
  2. Go to Customise dashboard.
  3. Paste the script under "Dashboard footer content".
  4. Paste the styles under "Dashboard styles".
  5. Save. Enjoy.

Script

<script>
/*
 Plugin name: OG image
 Description: Generate and download an Open Graph image for your post.
 Author: Robert Birming
 Author URI: https://robertbirming.com
*/

(function () {
    'use strict';

    const $headerContent = document.getElementById('header_content');
    const $insertMedia   = document.getElementById('upload-image');

    if (!$headerContent || !$insertMedia) return;

    const blogSlug = window.location.pathname.split('/').filter(Boolean)[0];

    function getPostTitle() {
        const raw = $headerContent.innerText || '';
        const match = raw.match(/title:\s*(.+)/i);
        return match ? match[1].trim() : '';
    }

    function getTokens() {
        const s = getComputedStyle(document.documentElement);
        const get = function (v) { return s.getPropertyValue(v).trim(); };

        const bg     = get('--bg')     || '#f3f5f7';
        const text   = get('--text')   || '#2b3138';
        const link   = get('--link')   || '#1a5cb8';
        const border = get('--border') || '#dde3ea';

        const rawFont = get('--font-body') || get('--font-main') || get('--font-secondary') || 'system-ui';
        const fontBody = rawFont.split(',')[0].replace(/['"]/g, '').trim();

        return { bg, text, link, border, fontBody };
    }

    function getCustomDomain() {
        const $viewBtn = document.getElementById('view-button');
        if (!$viewBtn) return null;
        const match = ($viewBtn.getAttribute('onclick') || '').match(/\/\/([^/'"]+)/);
        return match ? match[1] : null;
    }

    var STORAGE_KEY = 'og_blog_name';

    function getSavedBlogName() {
        try { return localStorage.getItem(STORAGE_KEY) || ''; } catch (e) { return ''; }
    }

    function saveBlogName(val) {
        try { localStorage.setItem(STORAGE_KEY, val); } catch (e) {}
    }

    // --- Inject "OG image" into the native link bar ---

    const $bar  = $insertMedia.parentElement;
    const $sep  = document.createTextNode(' | ');
    const $link = document.createElement('a');

    $link.href        = '#';
    $link.textContent = 'OG image';
    $link.addEventListener('click', function (e) {
        e.preventDefault();
        openModal();
    });

    $bar.appendChild($sep);
    $bar.appendChild($link);

    // --- Styles ---

    const $style = document.createElement('style');
    $style.textContent = `
        #og-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0, 0, 0, 0.4);
            z-index: 9999;
            display: none;
            align-items: center;
            justify-content: center;
            padding: 20px;
            box-sizing: border-box;
        }
        #og-modal {
            width: 100%;
            max-width: 660px;
            max-height: 90vh;
            overflow-y: auto;
            background: var(--background-color, #fff);
            color: var(--text-color, #444);
            border: 1px solid lightgrey;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
            box-sizing: border-box;
        }
        #og-head {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 12px 16px;
            border-bottom: 1px solid lightgrey;
        }
        #og-body {
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 14px;
        }
        #og-preview-wrap {
            width: 100%;
            aspect-ratio: 1200 / 630;
            overflow: hidden;
            border: 1px solid rgba(128, 128, 128, 0.2);
        }
        #og-preview-wrap svg {
            width: 100%;
            height: 100%;
            display: block;
        }
        .og-field {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }
        .og-field label {
            font-size: 0.8em;
            font-weight: 600;
            opacity: 0.55;
            text-transform: uppercase;
            letter-spacing: 0.06em;
        }
        .og-field input[type="text"] {
            width: 100%;
            padding: 6px 8px;
            border: 1px solid rgba(128, 128, 128, 0.3);
            border-radius: 3px;
            background: transparent;
            color: inherit;
            font-family: inherit;
            font-size: 0.95em;
            box-sizing: border-box;
        }
        .og-field input[type="text"]:focus {
            outline: none;
            border-color: var(--link-color, #1a5cb8);
        }
        .og-row {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 12px;
        }
        #og-emoji-strip {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            align-items: center;
        }
        .og-emoji-btn {
            width: 36px;
            height: 36px;
            border: 1px solid rgba(128, 128, 128, 0.25);
            border-radius: 3px;
            background: rgba(128, 128, 128, 0.07);
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0;
            color: inherit;
            font-family: inherit;
            font-size: 1.2em;
        }
        .og-emoji-btn.og-emoji-none {
            font-size: 0.75em;
            width: auto;
            padding: 0 8px;
        }
        .og-emoji-btn:hover {
            border-color: rgba(128, 128, 128, 0.45);
        }
        .og-emoji-btn.active {
            border-color: var(--link-color, #1a5cb8);
            background: rgba(128, 128, 128, 0.14);
        }
        #og-custom-emoji {
            width: 64px;
            height: 36px;
            padding: 0 6px;
            border: 1px solid rgba(128, 128, 128, 0.3);
            border-radius: 3px;
            background: transparent;
            color: inherit;
            font-family: inherit;
            font-size: 0.75em;
        }
        #og-custom-emoji:focus {
            outline: none;
            border-color: var(--link-color, #1a5cb8);
        }
        #og-actions {
            display: flex;
            align-items: center;
            justify-content: flex-end;
            gap: 10px;
            padding-top: 2px;
        }
        @media (prefers-color-scheme: dark) {
            #og-modal,
            #og-head {
                border-color: rgba(255, 255, 255, 0.12);
            }
        }
    `;
    document.head.appendChild($style);

    // --- Build modal DOM ---

    const $overlay = document.createElement('div');
    $overlay.id = 'og-overlay';

    const $modal = document.createElement('div');
    $modal.id = 'og-modal';

    const $head = document.createElement('div');
    $head.id = 'og-head';

    const $headTitle = document.createElement('strong');
    $headTitle.textContent = 'OG image';

    const $closeBtn = document.createElement('button');
    $closeBtn.type = 'button';
    $closeBtn.textContent = 'Close';
    $closeBtn.addEventListener('click', closeModal);

    $head.appendChild($headTitle);
    $head.appendChild($closeBtn);

    const $modalBody = document.createElement('div');
    $modalBody.id = 'og-body';

    const $previewWrap = document.createElement('div');
    $previewWrap.id = 'og-preview-wrap';

    // Blog name field
    const $blognameField = document.createElement('div');
    $blognameField.className = 'og-field';
    const $blognameLabel = document.createElement('label');
    $blognameLabel.textContent = 'Blog name';
    const $blognameCtrl = document.createElement('input');
    $blognameCtrl.type = 'text';
    $blognameCtrl.id = 'og-blogname';
    $blognameCtrl.placeholder = 'Your name or blog name';
    $blognameField.appendChild($blognameLabel);
    $blognameField.appendChild($blognameCtrl);

    // Title field
    const $titleField = document.createElement('div');
    $titleField.className = 'og-field';
    const $titleLabel = document.createElement('label');
    $titleLabel.textContent = 'Title';
    const $titleCtrl = document.createElement('input');
    $titleCtrl.type = 'text';
    $titleCtrl.id = 'og-title';
    $titleCtrl.placeholder = 'Post title';
    $titleField.appendChild($titleLabel);
    $titleField.appendChild($titleCtrl);

    // Tagline + domain row
    const $row = document.createElement('div');
    $row.className = 'og-row';

    const $taglineField = document.createElement('div');
    $taglineField.className = 'og-field';
    const $taglineLabel = document.createElement('label');
    $taglineLabel.textContent = 'Tagline';
    const $taglineCtrl = document.createElement('input');
    $taglineCtrl.type = 'text';
    $taglineCtrl.id = 'og-tagline';
    $taglineCtrl.placeholder = 'e.g. a personal blog';
    $taglineField.appendChild($taglineLabel);
    $taglineField.appendChild($taglineCtrl);

    const $domainField = document.createElement('div');
    $domainField.className = 'og-field';
    const $domainLabel = document.createElement('label');
    $domainLabel.textContent = 'Domain';
    const $domainCtrl = document.createElement('input');
    $domainCtrl.type = 'text';
    $domainCtrl.id = 'og-domain';
    $domainCtrl.placeholder = 'yourblog.com';
    $domainField.appendChild($domainLabel);
    $domainField.appendChild($domainCtrl);

    $row.appendChild($taglineField);
    $row.appendChild($domainField);

    // Emoji strip
    const $emojiField = document.createElement('div');
    $emojiField.className = 'og-field';
    const $emojiLabel = document.createElement('label');
    $emojiLabel.textContent = 'Accent graphic';
    const $emojiStrip = document.createElement('div');
    $emojiStrip.id = 'og-emoji-strip';

    var emojiOptions = ['', '✏️', '💬', '📚', '🎵', '🎙️', '🎬', '📷', '☕', '🌱', '❤️', '🐻'];
    emojiOptions.forEach(function (em) {
        var btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'og-emoji-btn' + (em === '' ? ' og-emoji-none active' : '');
        btn.dataset.emoji = em;
        btn.setAttribute('aria-label', em ? em + ' accent' : 'No accent graphic');
        btn.textContent = em || 'None';
        $emojiStrip.appendChild(btn);
    });

    const $customEmoji = document.createElement('input');
    $customEmoji.type = 'text';
    $customEmoji.id = 'og-custom-emoji';
    $customEmoji.placeholder = 'Custom';
    $emojiStrip.appendChild($customEmoji);

    $emojiField.appendChild($emojiLabel);
    $emojiField.appendChild($emojiStrip);

    const $actions = document.createElement('div');
    $actions.id = 'og-actions';

    const $dlBtn = document.createElement('button');
    $dlBtn.type = 'button';
    $dlBtn.textContent = 'Download PNG';
    $actions.appendChild($dlBtn);

    $modalBody.appendChild($previewWrap);
    $modalBody.appendChild($blognameField);
    $modalBody.appendChild($titleField);
    $modalBody.appendChild($row);
    $modalBody.appendChild($emojiField);
    $modalBody.appendChild($actions);

    $modal.appendChild($head);
    $modal.appendChild($modalBody);
    $overlay.appendChild($modal);
    document.body.appendChild($overlay);

    $overlay.addEventListener('click', function (e) {
        if (e.target === $overlay) closeModal();
    });

    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && $overlay.style.display === 'flex') closeModal();
    });

    // --- State ---

    var selectedEmoji = '';

    // --- Open / close ---

    function openModal() {
        $blognameCtrl.value  = getSavedBlogName();
        $titleCtrl.value     = getPostTitle();
        $domainCtrl.value    = getCustomDomain() || blogSlug + '.bearblog.dev';
        $taglineCtrl.value   = '';
        selectedEmoji = '';
        $emojiStrip.querySelectorAll('.og-emoji-btn').forEach(function (b) {
            b.classList.toggle('active', b.dataset.emoji === '');
        });
        $customEmoji.value = '';
        render();
        $overlay.style.display = 'flex';
    }

    function closeModal() {
        $overlay.style.display = 'none';
    }

    // --- SVG builder ---

    function escX(s) {
        return String(s)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    function wrapTitle(text, maxChars) {
        if (text.length <= maxChars) return [text];
        var words = text.split(' ');
        var lines = [];
        var current = '';
        words.forEach(function (w) {
            var candidate = current ? current + ' ' + w : w;
            if (candidate.length > maxChars) {
                if (current) lines.push(current);
                current = w;
            } else {
                current = candidate;
            }
        });
        if (current) lines.push(current);
        return lines;
    }

    function buildSVG(title, domain, tagline, blogname, emoji, tokens) {
        var W = 1200, H = 630;
        var bg     = tokens.bg;
        var text   = tokens.text;
        var link   = tokens.link;
        var border = tokens.border;
        var font   = tokens.fontBody;

        var lines = wrapTitle(title || 'Untitled', 34);
        lines = lines.slice(0, 3);
        var fs    = lines.length > 2 ? 56 : (title.length > 26 ? 64 : 74);
        var y0    = lines.length > 2 ? 180 : (lines.length === 2 ? 200 : 232);
        var lh    = fs * 1.22;

        var titleSVG = lines.map(function (l, i) {
            return '<text x="80" y="' + (y0 + i * lh) + '"'
                + ' font-family="' + escX(font) + ', system-ui, sans-serif"'
                + ' font-size="' + fs + '"'
                + ' font-weight="800"'
                + ' fill="' + escX(text) + '"'
                + ' letter-spacing="-0.02em">'
                + escX(l) + '</text>';
        }).join('\n  ');

        var lastBaseline = y0 + (lines.length - 1) * lh;
        var ruleY    = lastBaseline + 28;
        var taglineY = ruleY + 46;

        // Blog name — upper right corner
        var blognameSVG = blogname
            ? '<text x="' + (W - 80) + '" y="80"'
              + ' font-family="' + escX(font) + ', system-ui, sans-serif"'
              + ' font-size="14" font-weight="600" text-anchor="end"'
              + ' fill="' + escX(text) + '" opacity="0.4"'
              + ' letter-spacing="0.1em">'
              + escX(blogname.toUpperCase()) + '</text>'
            : '';

        // Tagline — below rule, same area as title
        var taglineSVG = tagline
            ? '<text x="80" y="' + taglineY + '"'
              + ' font-family="' + escX(font) + ', system-ui, sans-serif"'
              + ' font-size="30" font-weight="400"'
              + ' fill="' + escX(text) + '" opacity="0.55">'
              + escX(tagline.substring(0, 80)) + '</text>'
            : '';

        // Domain — bottom left, muted
        var domainSVG = domain
            ? '<text x="80" y="' + (H - 36) + '"'
              + ' font-family="' + escX(font) + ', system-ui, sans-serif"'
              + ' font-size="16" font-weight="400"'
              + ' fill="' + escX(text) + '" opacity="0.35">'
              + escX(domain) + '</text>'
            : '';

        // Emoji — bottom right
        var emojiSVG = emoji
            ? '<text x="' + (W - 80) + '" y="' + (H - 50) + '"'
              + ' font-size="160" text-anchor="end" opacity="0.22">' + emoji + '</text>'
            : '';

        return '<svg xmlns="http://www.w3.org/2000/svg"'
            + ' viewBox="0 0 ' + W + ' ' + H + '"'
            + ' width="' + W + '" height="' + H + '">\n'
            + '  <rect width="' + W + '" height="' + H + '" fill="' + escX(bg) + '"/>\n'
            + '  <rect x="0" y="0" width="6" height="' + H + '" fill="' + escX(link) + '" opacity="0.75"/>\n'
            + '  <rect x="1" y="1" width="' + (W - 2) + '" height="' + (H - 2) + '" fill="none"'
            + ' stroke="' + escX(border) + '" stroke-width="1"/>\n'
            + '  ' + blognameSVG + '\n'
            + '  ' + emojiSVG + '\n'
            + '  ' + titleSVG + '\n'
            + '  <line x1="80" y1="' + ruleY + '" x2="460" y2="' + ruleY + '"'
            + ' stroke="' + escX(border) + '" stroke-width="1"/>\n'
            + '  ' + taglineSVG + '\n'
            + '  ' + domainSVG + '\n'
            + '</svg>';
    }

    // --- Render into preview ---

    function render() {
        var tokens = getTokens();
        var svg    = buildSVG(
            $titleCtrl.value,
            $domainCtrl.value,
            $taglineCtrl.value,
            $blognameCtrl.value,
            selectedEmoji,
            tokens
        );
        $previewWrap.innerHTML = svg;
    }

    [$blognameCtrl, $titleCtrl, $domainCtrl, $taglineCtrl].forEach(function (el) {
        el.addEventListener('input', render);
    });

    $blognameCtrl.addEventListener('input', function () {
        saveBlogName(this.value);
    });

    // --- Emoji strip ---

    $emojiStrip.addEventListener('click', function (e) {
        var btn = e.target.closest('.og-emoji-btn');
        if (!btn) return;
        $emojiStrip.querySelectorAll('.og-emoji-btn').forEach(function (b) {
            b.classList.remove('active');
        });
        btn.classList.add('active');
        selectedEmoji = btn.dataset.emoji;
        $customEmoji.value = '';
        render();
    });

    $customEmoji.addEventListener('input', function () {
        var val = this.value.trim();
        var chars = Array.from(val);
        selectedEmoji = chars[0] || '';
        $emojiStrip.querySelectorAll('.og-emoji-btn').forEach(function (b) {
            b.classList.remove('active');
        });
        render();
    });

    // --- PNG download via SVG → Canvas ---

    $dlBtn.addEventListener('click', function () {
        var tokens = getTokens();
        var title  = $titleCtrl.value || 'og-image';
        var svgStr = buildSVG(
            $titleCtrl.value,
            $domainCtrl.value,
            $taglineCtrl.value,
            $blognameCtrl.value,
            selectedEmoji,
            tokens
        );

        var blob = new Blob([svgStr], { type: 'image/svg+xml' });
        var url  = URL.createObjectURL(blob);
        var img  = new Image();

        img.onload = function () {
            var canvas = document.createElement('canvas');
            canvas.width  = 1200;
            canvas.height = 630;
            canvas.getContext('2d').drawImage(img, 0, 0, 1200, 630);
            URL.revokeObjectURL(url);
            canvas.toBlob(function (pngBlob) {
                var a = document.createElement('a');
                var pngUrl = URL.createObjectURL(pngBlob);
                a.href = pngUrl;
                a.download = slugify(title) + '.png';
                a.click();
                setTimeout(function () { URL.revokeObjectURL(pngUrl); }, 100);
            }, 'image/png');
        };

        img.src = url;
    });

    function slugify(s) {
        var base = String(s).normalize('NFD').replace(/[\u0300-\u036f]/g, '')
            .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
        return base || 'og-image';
    }

})();
</script>

Styles

:root {
  --bg: #f8f8f8;
  --text: #2c2d2e;
  --link: #1f5fbf;
  --font-body: system-ui, sans-serif;
  --border: #d8dde3;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1c1f23;
    --text: #e6e7e8;
    --link: #74b0f4;
    --border: #2e3540;
  }
}

Happy blogging, OG style.

If you can't use scripts with your Bear Blog, you might want to check out this feature image generator by Another Dayu.

Want more? Check out all available Bearming add-ons.