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

Talk to a real person

Live human support. Choose what you need help with.