Self-hosted server to automatically generate unique OG images for every page of your website.
The /capture
endpoint generates a screen capture for any URL you pass in via the url
query parameter.
<meta property="og:image" content="https://your-server/capture?url=turso.tech/pricing" />
Before | After |
---|---|
The /template
endpoint renders any static web build using variables passed in via query parameters.
<meta property="og:image" content="https://your-server/template/example?title=Tire+Dust+Makes+Up+The+Majority+of+Ocean+Microplastics&subhead=Researchers+say+tire+emissions+pose+a+threat+to+global+health%2C+and+EVs+could+make+the+problem+worse.&author=Lewin+Day&date=September+28%2C+2023&img=https%3A%2F%2Fthedrive.com%2Fuploads%2F2023%2F09%2F28%2FGettyImages-1428297317.jpg" />
Before | After |
---|---|
Download and run the latest binary from the releases page or use the command below. You must have Chrome or Chromium installed on your system.
Use the --update
flag to update to the latest version.
curl -sL "https://github.com/henrygd/social-image-server/releases/latest/download/social-image-server_$(uname -s)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" | tar -xz -O social-image-server | tee ./social-image-server >/dev/null && chmod +x social-image-server && ls social-image-server
See the example docker-compose or run the command below to try it out.
docker run --rm --init -p 8080:8080 henrygd/social-image-server
The image is bundled with a slimmed down Chrome, but remains relatively lightweight -- about 10% the size of browserless/chrome
-- and doesn't require a ton of resources.
Note: The only font included in the image is DejaVu. If you want to use a different fallback font for websites that don't provide them, you need to mount your fonts in the container. See the docker-compose for an example.
Add an og:image
meta tag in the <head>
of your website that points to your server.
<meta property="og:image" content="https://yourserver.com/capture?url=example.com/current-page" />
It's best to add this once in a layout template, rather than doing it for every page. See Framework Examples.
A useful site for previewing or generating boilerplate HTML is metatags.io.
Templates are just static webpages -- like the output of vite build
-- that render URL query parameters in the content.
You can use any web framework to create templates. I made the example above using Vite, Svelte, and Tailwind (view relevant code). The build command generates the static files.
To add a template, create a folder containing your files in the data/templates
directory. If your folder is called my-template
, it would then be available at /template/my-template
.
A url
query parameter is still required for templates. It's used to prevent abuse by verifying that the requested image matches the image used on the origin URL. If you're just testing, use _regen_
to skip verification and a dummy string like "test" as the url.
Please ensure that your query parameters are encoded in your request. You can use encodeURIComponent
in JavaScript, url.QueryEscape
in Go, urlencode
in PHP, urllib.parse.quote
in Python, URLEncoder.encode
in Java, etc.
You can refresh the cache for an image by changing any query parameter (or template name if applicable) in the origin HTML. If you're just testing, use the _regen_
parameter.
If incoming request parameters don't match the cache, the server will verify that params on the origin URL have changed and generate a new image if so.
See Framework Examples for examples of a version parameter that automatically refreshes the cache on new site builds.
Name | Default | Description |
---|---|---|
url |
- | URL to generate image for and verify against. |
width |
1400 | Width of browser viewport in pixels (max 2500). Output image is scaled to IMG_WIDTH width. |
delay |
0 | Delay in milliseconds after page load before generating image. |
dark |
false | Sets prefers-color-scheme to dark. |
format |
- | Image format. Defaults to IMG_FORMAT value if not specified. |
_regen_ |
- | Do not use in public URLs. Testing only. Skips origin verification and forces full regeneration on every request. Must match REGEN_KEY value. |
Name | Default | Description |
---|---|---|
ALLOWED_DOMAINS |
- | Restrict to certain domains. Example: "example.com,example.org" |
CACHE_TIME |
30 days | Time to cache images on server. Minimum 1 hour. |
DATA_DIR |
./data | Directory to store program data (images and database). |
FONT_FAMILY |
- | Change browser fallback font. Must be available on your system / image. |
IMG_FORMAT |
jpeg | Default format if not specified in request. Valid values: "jpeg", "png". |
IMG_QUALITY |
92 | Compression quality (jpeg only). |
IMG_WIDTH |
2000 | Width of output image in pixels. |
LOG_LEVEL |
info | Logging level. Valid values: "debug", "info", "warn", "error". |
MAX_TABS |
5 | Maximum number of active browser tabs. 2 or 3 is fine in most cases. |
PERSIST_BROWSER |
5m | Time to keep the browser process running after the last image generation. Valid units: "ms", "s", "m", "h". See FAQ for more info. |
PORT |
8080 | Port to listen on. |
REGEN_KEY |
- | Key used to force bypass cache. |
REMOTE_URL |
- | Connect to an existing Chrome or Chromium instance using WebSocket. Example: wss://localhost:9222 |
Only if you use REMOTE_URL
to connect with a remote browser process. Otherwise the server will manage headless browser processes as necessary.
If it needs to generate an image and there is no process already running, it will start one. After the image is generated, a timer is started. If another image generation occurs within the timer duration, the browser process is reused, and the timer is reset. If the timer reaches zero, the browser process is terminated.
This way active servers get the performance benefits of reusing the browser process, while less active servers will not need to keep Chrome running in the background forever.
The timer duration can be configured with the 10PERSIST_BROWSER
environment variable.
The server's outgoing request to websites always includes the URL parameter og-image-request=true
, so check for that. Add a short delay if you're doing the check on the front end.
Probably because the website isn't providing fonts over the network, and the browser has a different default font than your personal setup.
You should be able to change the font using the FONT_FAMILY
environment variable. If using docker, mount your fonts in the container. Check the docker-compose for a commented-out example.
If you're using a remote browser (not recommended), try setting the --system-font-family
flag on the process.
From what I can tell, Facebook and LinkedIn (and likely others) don't support webp open graph images. If I'm wrong, let me know and I'll add it.
Twitter seems to cache images for a long time. Try adding or removing a trailing slash, query parameter, or fragment to the URL in your tweet.
- Do not run a public server without setting
ALLOWED_DOMAINS
. Without restrictions, an attacker can use your browser to visit a malicious URL. - Do not leak your regen key in your HTML. The regen key force bypasses the cache and URL status verification, so an attacker can attempt to DoS the server by sending thousands of requests to different URL paths. If you think you may have leaked it, change the
REGEN_KEY
environment variable or remove it entirely. - Keep
MAX_TABS
to a reasonable value. Your OG images are cached both on the server and usually by the service you're sharing to, so it's unlikely that you'll be handling lots of simultaneous image generations. Most servers will be fine with 2 or 3 max tabs. If all tabs are in use, new requests are just queued until one of the tabs is free.
Use the REMOTE_URL
environment variable to connect to a remote instance of Chrome or Chromium over WebSocket.
Important
This approach is only recommended if you already have an existing browser process running full time. The server cannot stop / start the process, so it will need to run independently for the lifetime of the server.
I'd also recommend using the binary, since it doesn't come bundled with its own browser, though I can make a tiny docker image with only the server if there's any demand for it.
If you're using a container image for Chrome, check the documentation for a port (usually 9222) or address to connect to. If you're using the server binary, expose the Chrome container's port to the host: 127.0.0.1:9222:9222
. If using the docker version, put the container in the same network as Chrome and connect using its container name: wss://chrome-container:9222
.
If using Chrome directly, set the --remote-debugging-port
flag. Note that if you're running the server as a container you will need to give it access to your host ports.
google-chrome-stable --remote-debugging-port=9222 --headless=new --hide-scrollbars --font-render-hinting=none --disable-background-networking --enable-features=NetworkService,NetworkServiceInProcess --disable-extensions --disable-breakpad --disable-backgrounding-occluded-windows --disable-default-apps --disable-background-timer-throttling --disable-features=site-per-process,Translate,BlinkGenPropertyTrees --disable-hang-monitor --disable-client-side-phishing-detection --disable-popup-blocking --disable-prompt-on-repost --disable-sync --disable-translate --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain
The server includes status headers for successful image requests.
Value | Description |
---|---|
HIT | Cached image was served |
MISS | New image was generated |
Value | Description |
---|---|
0 | New image generated because it did not exist in cache |
1 | New image generated due to _regen_ parameter |
2 | Found matching cached image |
3 | Request does not match og:image on origin URL. Using previously cached image. |
These examples use a query parameter v
to bypass cache on new builds, but you can remove it if you don't need that functionality. Feel free to improve these or contribute others.
Make sure you set your website's origin in prerender settings.
<!-- +layout.svelte -->
<script>
import { version } from '$app/environment'
import { page } from '$app/stores'
</script>
<sveltekit:head>
<meta
property="og:image"
content="https://your-server/capture?url={$page.url.toString()}&v={version}"
/>
</sveltekit:head>
<!-- Layout.astro -->
---
import {version} from '../../package.json';
---
<head>
<meta
property="og:image"
content={`https://your-server/capture?url=${Astro.url}&v=${version}`}
/>
</head>
// header.php
<?php
$page_url = home_url($_SERVER['REQUEST_URI']);
$version = wp_get_theme()->get('Version');
$og_image_url = "https://your-server/capture?url=$page_url&v=$version";
?>
<head>
<meta property="og:image" content="<?php echo $og_image_url ?>"/>
</head>