Skip to main content

Social share card generator

Marketing teams burn hours making the same graphic over and over: a globe, a number, a headline, the logo bottom-right. This makes it one function call, driven by live data, at whatever size the platform wants.

The two pieces that make it work are title and watermark: both are painted onto the canvas, so the exported PNG is finished art rather than something you still have to take into a design tool.

Loading globe…

The code

import { createGlobe } from "canvas-globe";

function shareCard({ countries, teams, markers }) {
const canvas = document.createElement("canvas");

const globe = createGlobe(canvas, {
preset: "aurora",
autoRotate: false,
interactive: false,
center: { lon: 20, lat: 18 },
radiusRatio: 0.36,

markers,
markerStyle: "dot",

title: {
text: `Now in ${countries} countries`,
subtitle: `${teams.toLocaleString()} teams shipping with Acme`,
position: "top-center",
},
watermark: { image: "/logo.svg", text: "acme.com", position: "bottom-right" },
});

const png = globe.exportImage({ preset: "og" }); // 1200 × 630 data URL
globe.destroy();
return png;
}

exportImage renders one frame off-screen at the requested size: it does not touch the visible canvas, so you can generate a 1200 × 630 card from a globe that is 320 px wide on screen.

Sizes

PresetPixelsFor
og1200 × 630Open Graph: Facebook, Slack, LinkedIn unfurls
twitter1600 × 900X / Twitter summary card
linkedin1200 × 627LinkedIn posts
square1080 × 1080Instagram feed
story1080 × 1920Instagram and LinkedIn stories
portrait1080 × 1350Instagram portrait
wide1920 × 1080Slides and video thumbnails
thumbnail640 × 640Cards inside your own app

Or ask for exact pixels: globe.exportImage({ width: 2400, height: 1260 }).

:::tip Type scales with the canvas title.size and watermark.size default to a fraction of the smaller canvas side, so one config produces sensible type at 630 px and at 1920 px. Only set explicit sizes when you are targeting a single output. :::

Cards per page, per customer, per week

The point of generating these is that you can make thousands.

Per marketing page: a different centre and headline for each geo landing page:

const card = shareCard({
countries: region.countries,
teams: region.teams,
markers: region.cities,
});

Per customer, in-product: the "your year with us" graphic people actually post:

globe.setOptions({
title: { text: `${user.name} visited 14 countries`, subtitle: "2024 in review" },
watermark: { image: "/logo.svg", text: "acme.com" },
markers: user.trips,
});
const blob = await globe.exportBlob({ preset: "square" });

Then hand it straight to the share sheet: no upload round trip:

const file = new File([blob], "2024.png", { type: "image/png" });
if (navigator.canShare?.({ files: [file] })) {
await navigator.share({ files: [file], title: "My 2024" });
}

Weekly, in CI: regenerate the OG image from live numbers so the unfurl is never stale.

Build-time and server-side

exportImage needs a document, so it returns null in plain Node. Two ways round it:

// 1. Headless browser at build time: the reliable option.
const png = await page.evaluate(() => window.globe.exportImage({ preset: "og" }));
await fs.writeFile("static/og/index.png", Buffer.from(png.split(",")[1], "base64"));
// 2. A DOM shim, if your host allows it.
import { JSDOM } from "jsdom";
// …provide document + a canvas implementation before importing the package.

For most teams option 1 inside a Playwright script is a twenty-line build step and it produces exactly what a browser produces.

Transparent exports

If the card is going into a design template that already has a background, drop the ocean fill:

globe.exportImage({ preset: "square", transparent: true });

Set transparentBackground: true on the live globe too if it sits on a coloured section.

Why bake the text in?

The obvious alternative is HTML text over the canvas, which is better for accessibility and selectable copy. Do that on the page. But an OG image is a single flat PNG: there is no HTML layer at the other end. Anything not on the canvas is not in the card.

So: HTML headline for the live page, title for the export. Setting both and hiding one is a perfectly reasonable amount of work for a graphic that goes out ten thousand times.

// Live page: HTML heading, no canvas title.
// Export: swap it in for one frame.
globe.setOptions({ title: { text: headline, subtitle } });
const png = globe.exportImage({ preset: "og" });
globe.setOptions({ title: null });

Next

  • Exporting: every size, transparency and video recording
  • Overlays: the full title and watermark reference
  • Year in review: the animated version of the same idea