We can't find the internet
Attempting to reconnect
Using JavaScript in Templates
Templates run in a real browser. You can use Alpine.js, animations, and any JS library via CDN.
OpenGraph templates are rendered in a headless Chrome instance before being screenshotted. This means JavaScript executes — your <script> tags run, DOM manipulation works, and external libraries load from CDNs.
Page data is available through Liquid variables (see the Template Authoring Guide), not through a JavaScript API. Use Liquid to write data into the HTML, then use JS to enhance the layout.
What Works
-
<script>tags (inline and external via CDN) - Alpine.js, Petite Vue, or any lightweight framework
- CSS animations and transitions (captured at screenshot time)
- Google Fonts and external stylesheets
- Canvas and SVG manipulation
Alpine.js Example
A product card that conditionally shows a sale badge and formats the layout with Alpine:
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
<div
x-data='{
name: "{{ Product.name | default: title }}",
brand: "{{ Product.brand.name }}",
price: "{{ Product.offers.price }}",
currency: "{{ Product.offers.priceCurrency | default: "USD" }}",
available: "{{ Product.offers.availability }}" !== "",
get onSale() { return this.price && parseFloat(this.price) < 50 },
get formattedPrice() {
if (!this.price) return null;
return new Intl.NumberFormat("en-US", { style: "currency", currency: this.currency }).format(this.price);
}
}'
style="width: 1200px; height: 630px; display: flex; align-items: center; justify-content: center; background: #18181b; font-family: system-ui; padding: 80px; color: white;"
>
<div>
<p x-show="brand" x-text="brand" style="font-size: 16px; text-transform: uppercase; letter-spacing: 2px; color: #a1a1aa;"></p>
<h1 x-text="name" style="font-size: 52px; line-height: 1.1; margin-top: 8px;"></h1>
<div style="display: flex; align-items: center; gap: 16px; margin-top: 24px;">
<span x-show="formattedPrice" x-text="formattedPrice" style="font-size: 36px; font-weight: 700;"></span>
<span x-show="onSale" style="background: #ef4444; padding: 4px 12px; border-radius: 4px; font-size: 14px; font-weight: 600;">SALE</span>
</div>
<p style="font-size: 18px; color: #71717a; margin-top: 32px;">{{ domain }}</p>
</div>
</div>
This uses Liquid to inject the raw data, then Alpine to format the price with Intl.NumberFormat and conditionally show a sale badge — logic that would be harder to express in Liquid alone.
CSS Animations
How animation is treated depends on the origin’s output format.
Static renders (png / jpeg / webp) capture a single frame. If you want an animation to be visible in a static OG image, make sure the interesting state is the default state or use animation-fill-mode: forwards with a short duration so it completes before the screenshot.
<style>
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate {
animation: fade-in 0.1s ease-out forwards;
}
</style>
<div style="width: 1200px; height: 630px; background: #18181b; display: flex; align-items: center; justify-content: center;">
<h1 class="animate" style="font-size: 52px; color: white; font-family: system-ui;">
{{ title }}
</h1>
</div>
Animated origins (format: gif or animated_webp) capture a timeline of frames instead of a single one. Animated output is set at the origin/render level — not from the page — and the template declares its timeline via window.__og:
<script>
window.__og = [
['animate', { duration: 3000, fps: 12 }],
['capture', '#card'],
];
</script>
For video, the template also implements an optional async hook window.__ogFrame(t) that seeks a <video> element to time t and resolves once that frame is ready — the pipeline calls it once per captured frame.
Animation (GIF and animated WebP) is a paid feature; MP4 is not supported. For the format option, the plan gate, capture strategies, and timing knobs, see the animation topic. The deep authoring guide for sizing and layout is the Template Authoring Guide.
Tips
-
Use
deferon script tags. This ensures the HTML is parsed before JS runs. - Keep it fast. The browser waits for the page to settle before screenshotting, but heavy JS can slow down image generation.
- Liquid first, JS second. Use Liquid for simple variable injection and conditionals. Reach for JS when you need formatting, math, or complex conditional logic.
- Test in a real browser. Open the template HTML as a local file or paste into a CodePen to debug JS issues before uploading.