WP OG Takumi
WordPress OG image generation via Takumi (Rust FFI) — Tailwind CSS templates rendered to PNG natively
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/rbbydotdev/wp-og-takumi/archive/refs/heads/master.zipDynamic Open Graph image generation for WordPress using Takumi (Rust) via PHP FFI.
Write your OG image templates in HTML with Tailwind CSS classes. Takumi handles flexbox layout, text rendering, gradients, and outputs a PNG. No headless browser, no external API, no ImageMagick -- native speed, in-process.
How it works
Template (HTML + tw attributes + {{variables}})
|
v
PHP template engine
|-- resolves template (per-post > per-type > global > file)
|-- substitutes {{title}}, {{excerpt}}, etc. from WordPress
|-- parses HTML into a JSON node tree
|
v
Rust FFI (Takumi)
|-- deserializes JSON into Takumi Node tree
|-- Takumi does flexbox layout + Tailwind styling + text shaping
|-- outputs PNG bytes (1200x630)
|
v
Cached PNG served via REST endpoint
GET /wp-json/wp-og-takumi/v1/og-image/{post_id}
On the frontend, <meta property="og:image"> tags are injected into wp_head on singular pages, pointing at the REST endpoint.
Template format
Templates are HTML with tw (Tailwind CSS) attributes. Takumi supports the full Tailwind utility set -- flexbox, spacing, typography, colors, gradients, opacity, etc.
<div tw="w-[1200px] h-[630px] flex items-center justify-center"
style="background: linear-gradient(135deg, #C4653A, #1B6B6D)">
<div tw="flex flex-col items-center text-white p-16 text-center">
<span tw="text-lg uppercase tracking-widest text-white/70 mb-4">{{post_type_label}}</span>
<h1 tw="text-6xl font-bold text-white leading-tight">{{title}}</h1>
<p tw="text-2xl text-white/80 mt-6">{{excerpt}}</p>
<div tw="flex items-center mt-8 gap-4">
<span tw="text-lg font-semibold text-white">{{site_name}}</span>
<span tw="text-lg text-white/70">{{date}}</span>
</div>
</div>
</div>
This is the same syntax Takumi uses natively. The PHP side just parses the HTML into a JSON node tree and passes it through -- Takumi handles all layout and rendering.
Available variables
| Variable | Source | Scope |
|---|---|---|
{{title}} |
Post title | All |
{{excerpt}} |
Excerpt (trimmed 160 chars) | All |
{{author}} |
Author display name | All |
{{date}} |
Formatted publish date | All |
{{post_type_label}} |
"Post", "Page", etc. | All |
{{site_name}} |
Blog name | All |
{{featured_image}} |
Featured image file path | All |
{{categories}} |
Comma-separated | Posts |
Add your own variables by editing getVariables() in includes/class-og-template-engine.php.
Template cascade
Templates resolve in this order (first match wins):
- Per-post meta (
_og_templatepost meta, set via the editor meta box) - Per-post-type option (Settings > OG Images, one tab per post type)
- Global default option (Settings > OG Images, "Global Default" tab)
- File template (
templates/{post_type}.html, thentemplates/default.html)
Requirements
- Docker (for building the Rust shared library)
- WordPress 6+ with PHP 8.4+
- That's it. No local Rust toolchain needed.
Setup (step by step)
1. Copy the plugin into your WordPress project
cp -r wp-og-takumi/ /path/to/your/wp-content/plugins/wp-og-takumi/
2. Download fonts
The plugin needs TTF font files for text rendering. A download script is included:
cd wp-content/plugins/wp-og-takumi/fonts
sh ../scripts/download-fonts.sh
This downloads static TTFs from Google Fonts (Playfair Display + Source Sans 3). To use different fonts, edit the script or drop your own .ttf files into fonts/.
3. Add the Dockerfile to your project root
Copy docker/Dockerfile to your project root (or adapt it into your existing Dockerfile):
###############################################################################
# Stage 1: Compile the Rust shared library
###############################################################################
FROM rust:1.94-bookworm AS rust-builder
WORKDIR /build
COPY wp-content/plugins/wp-og-takumi/takumi-og-ffi/ .
COPY wp-content/plugins/wp-og-takumi/fonts/ /build/../fonts/
RUN cargo test --release
RUN cargo build --release \
&& cp target/release/libwp_og_takumi_ffi.so /build/libwp_og_takumi.so
###############################################################################
# Stage 2: WordPress with PHP FFI
###############################################################################
FROM wordpress:6-php8.4-apache
RUN apt-get update \
&& apt-get install -y --no-install-recommends libffi-dev \
&& docker-php-ext-install ffi \
&& rm -rf /var/lib/apt/lists/*
COPY --from=rust-builder /build/libwp_og_takumi.so /usr/local/lib/libwp_og_takumi.so
RUN ldconfig
RUN mkdir -p /var/www/html/wp-content/plugins/wp-og-takumi/lib
COPY --from=rust-builder /build/libwp_og_takumi.so \
/var/www/html/wp-content/plugins/wp-og-takumi/lib/libwp_og_takumi.so
RUN echo "ffi.enable=true" > /usr/local/etc/php/conf.d/ffi.ini
4. Update docker-compose.yml
services:
wordpress:
build: . # instead of image: wordpress:...
volumes:
- ./wp-content/plugins/wp-og-takumi:/var/www/html/wp-content/plugins/wp-og-takumi
5. Build and start
docker compose build
docker compose up -d
The Docker build compiles the Rust library, runs its tests, and produces a WordPress image with the .so baked in. No Rust toolchain needed on your machine.
6. Activate the plugin
Go to WP Admin > Plugins > Activate "WP OG Takumi".
7. (Optional) Build the admin JS
If you want the CodeMirror 6 template editor in wp-admin:
cd wp-content/plugins/wp-og-takumi
npm install
npm run build
Without this, the plugin still works -- you just edit templates as raw HTML in textareas instead of a syntax-highlighted editor.
Admin UI
Settings > OG Images -- tabbed interface with a CodeMirror 6 editor per post type:
- Heading Font / Body Font dropdowns -- 20 Google Fonts to choose from, or "Use theme font" to inherit from the WordPress Customizer
- Format button -- prettifies the template HTML
- Reset to Default -- restores the file-based template
- Insert Image -- opens WordPress media library, inserts
<img>at cursor - Preview -- renders the template to a real PNG and displays it inline
Per-post meta box (on any post/page edit screen):
- Check "Use custom OG template" -- editor prefills with the default template for that post type
- Same editor + toolbar
- Preview uses the actual post's data (title, excerpt, featured image, etc.)
Fonts
Fonts are configurable from the admin -- no code changes needed.
Settings > OG Images has two dropdowns at the top of the page:
| Setting | Default | What it controls |
|---|---|---|
| Heading Font | Use theme font | <h1> through <h6> elements |
| Body Font | Use theme font | <span>, <p>, and all other text |
20 Google Fonts are available: Playfair Display, Lora, Merriweather, Source Sans 3, Inter, Poppins, Montserrat, etc.
"Use theme font" pulls from your WordPress Customizer settings (get_theme_mod). If you change fonts in the Customizer, OG images pick them up automatically.
When you select a font, the plugin:
- Auto-downloads the TTF files from Google Fonts (cached in
fonts/) - Injects
font-['Family_Name']into the Tailwind classes of text elements - Clears all cached OG images so they re-render with the new font
You can also use custom fonts by dropping .ttf files into fonts/ and referencing them in your template with font-['Your_Font'] in the tw attribute.
Images
Templates can embed images using <img> tags. The plugin resolves image sources automatically:
<!-- WordPress upload URL -- auto-resolved to local file path -->
<img src="http://yoursite.com/wp-content/uploads/2024/photo.jpg" tw="w-[400px] h-[300px]" />
<!-- Featured image variable -- resolved from post thumbnail -->
<img src="{{featured_image}}" tw="absolute inset-0 w-full h-full object-cover" />
The Rust FFI reads local files and converts them to base64 data URIs for Takumi. WordPress upload URLs are automatically mapped to filesystem paths.
Background image pattern (used by the tour template):
<div tw="w-[1200px] h-[630px] flex relative">
<img src="{{featured_image}}" tw="absolute inset-0 w-full h-full object-cover" />
<div tw="absolute inset-0 bg-black/50"></div>
<div tw="relative flex flex-col p-16 text-white">
<h1 tw="text-5xl font-bold">{{title}}</h1>
</div>
</div>
This layers: full-bleed background photo, dark semi-transparent overlay, then text content on top.
Cache
OG images are cached as PNG files in wp-content/uploads/og-images/{post_id}.png.
Cache is automatically invalidated when:
| Event | What's cleared |
|---|---|
| Post saved (title, content, meta) | That post's cached PNG |
| OG template settings changed | All cached PNGs |
| Font settings changed | All cached PNGs |
| Theme fonts changed (Customizer) | All cached PNGs |
The next request to /wp-json/wp-og-takumi/v1/og-image/{id} after invalidation re-renders with fresh data.
Adapting for your site
Custom variables
Edit getVariables() in includes/class-og-template-engine.php to add variables for your custom post types:
if ($post->post_type === 'product') {
$vars['price'] = get_post_meta($post_id, '_price', true);
$vars['sku'] = get_post_meta($post_id, '_sku', true);
}
Then use {{price}} and {{sku}} in your templates.
Custom templates
Add a file at templates/{post_type}.html for any post type. The plugin picks it up automatically via the cascade.
Testing
Four test layers, run with make:
make test-php # PHPUnit (host, no Docker needed)
make test-rust # Rust unit tests (host, needs Rust)
make test-ffi # FFI smoke test (Docker)
make test-integration # Full WordPress integration (Docker)
make test # All of the above
How the Rust FFI works
The plugin uses PHP's built-in FFI extension to call a Rust shared library directly -- no subprocess, no HTTP, no CLI. The Rust side is a thin wrapper around Takumi:
PHP Rust (.so)
| |
| og_render(json, fonts) |
|--------------------------->|
| | 1. Deserialize JSON into Takumi Node tree
| | 2. Parse tw="" strings into TailwindValues
| | 3. Load .ttf fonts from directory
| | 4. Takumi: flexbox layout + render
| | 5. Encode to PNG bytes
| <-- PNG bytes |
| |
| og_free(ptr, len) |
|--------------------------->| Free the PNG buffer
The JSON format maps 1:1 to Takumi's Node API:
{
"type": "container",
"tw": "w-[1200px] h-[630px] flex items-center justify-center",
"style": "background: linear-gradient(135deg, #C4653A, #1B6B6D)",
"children": [
{
"type": "text",
"content": "Hello World",
"tw": "text-6xl font-bold text-white"
}
]
}
Three node types: container (layout), text (text content), image (embedded image).
The Rust FFI crate is ~180 lines. The C header exposes three functions:
uint8_t *og_render(const char *json_ptr, size_t json_len,
const char *font_dir_ptr, size_t font_dir_len,
size_t *out_len);
void og_free(uint8_t *ptr, size_t len);
const char *og_last_error(void);
File structure
wp-og-takumi.php Plugin bootstrap
includes/
class-og-template-engine.php Template cascade, variable extraction, HTML -> JSON
class-og-renderer.php PHP FFI bridge to Rust, PNG caching
class-og-meta.php wp_head OG/Twitter meta tags
class-og-endpoint.php REST: GET og-image/{id} + POST og-preview
class-og-admin.php Settings page + per-post meta box
src/
og-admin.ts CodeMirror 6 editor + preview (built with @wordpress/scripts)
templates/
default.html Fallback template
tour.html Tour-specific (price, duration, location)
post.html Blog posts (categories, author)
page.html Pages (minimal)
destination.html Destination-specific
guide.html Travel guide (author, date)
takumi-og-ffi/
Cargo.toml Rust crate: takumi 1.0.0-beta.7, cdylib
src/lib.rs FFI: JSON -> Takumi Node tree -> PNG
lib/
takumi_og.h C header for PHP FFI
fonts/ Static TTFs (downloaded via scripts/download-fonts.sh)
tests/ PHPUnit, FFI smoke test, integration test
docker/
Dockerfile Multi-stage: Rust builder + WordPress + PHP FFI
Makefile Build, test, font download commands
License
MIT