Generate Dynamic OG Images in Go with OGPeek API
Go developers building web applications with Gin, Echo, or Fiber all run into the same problem: generating social preview images means either shelling out to headless Chrome—burning hundreds of megabytes of RAM for a single screenshot—or wrestling with low-level image libraries that turn a simple task into a weekend project. OGPeek eliminates all of that. It is a URL-based API: construct a URL with your title and branding parameters, make a standard HTTP GET request, and receive a production-ready 1200×630 PNG. This guide covers every Go integration pattern—from basic net/http calls to Gin, Echo, and Fiber handlers, concurrent batch generation with goroutines, and a Cobra CLI tool for your build pipeline.
Why Go Apps Need Dynamic OG Images
Open Graph images are the thumbnails that appear when someone shares a link on Twitter, LinkedIn, Slack, or Discord. The platform fetches your page’s HTML, reads the <meta property="og:image"> tag, and renders the image in a rich preview card. Without one, your link is a plain text URL that disappears into the feed.
The impact is measurable: links with image previews receive 2–3x higher click-through rates than links without them. If you are building a web application in Go—whether it is a blog platform, a SaaS dashboard, or a developer tool—every link your users share without a proper OG image is leaving engagement on the table.
The traditional approaches in the Go ecosystem all have significant downsides:
- fogleman/gg and image/draw require manual text layout, font loading, coordinate math, and line-wrapping logic. What should be a five-minute task becomes hundreds of lines of pixel-pushing code that breaks when titles are longer than expected.
- Headless Chrome via chromedp or rod requires a full Chromium binary in your container. That means 400MB+ Docker images, cold-start times of several seconds, and a heavy dependency that complicates CI/CD pipelines and increases your attack surface.
- Static images mean every shared link gets the same generic preview. A blog post about goroutines and a blog post about database optimization look identical when shared—and neither gets clicked.
- Third-party screenshot services add latency, require API keys and account management, and charge per-screenshot fees that scale linearly with traffic.
A URL-based API eliminates all of this. You build a URL string with your title and branding parameters, the API returns a production-ready PNG, and the image is cached at the CDN edge. Your Go code does zero image processing, imports zero native libraries, and manages zero image files on disk.
How OGPeek Works
OGPeek is a URL-based API that generates Open Graph images from query parameters. No API key is required for the free tier (50 images per day, watermarked). A complete request URL looks like this:
https://todd-agent-prod.web.app/api/v1/og
?title=My+Go+App
&subtitle=Built+with+Go+1.22
&template=gradient
&theme=dark
&brandColor=%23F59E0B
The parameters control the generated image:
title— Primary headline text. Keep it under 60 characters for best results.subtitle— Secondary text below the title. Author name, category, read time, or a tagline.template— Visual layout:gradient,minimal,split, and more.theme— Color scheme:dark,midnight,dawn,slate.brandColor— Your accent color as a URL-encoded hex value.%23F59E0Bdecodes to#F59E0B.
Because it is a standard GET request that returns a PNG, any social media crawler can fetch it directly. No JavaScript rendering, no authentication, no cookies. The URL is the image.
Basic HTTP Client with net/http
Go’s standard library is all you need to call OGPeek. The net/http package provides a production-grade HTTP client, and net/url handles query parameter encoding. Here is a clean, reusable function that builds the URL and fetches the image bytes:
package ogpeek
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const baseURL = "https://todd-agent-prod.web.app/api/v1/og"
// Params holds the configuration for an OG image request.
type Params struct {
Title string
Subtitle string
Template string
Theme string
BrandColor string
}
// DefaultParams returns sensible defaults for OG image generation.
func DefaultParams(title string) Params {
return Params{
Title: title,
Template: "gradient",
Theme: "dark",
BrandColor: "#F59E0B",
}
}
// BuildURL constructs the full OGPeek API URL with encoded query parameters.
func BuildURL(p Params) string {
params := url.Values{}
params.Set("title", p.Title)
params.Set("template", p.Template)
params.Set("theme", p.Theme)
params.Set("brandColor", p.BrandColor)
if p.Subtitle != "" {
params.Set("subtitle", p.Subtitle)
}
return fmt.Sprintf("%s?%s", baseURL, params.Encode())
}
// client is a shared HTTP client with a reasonable timeout.
var client = &http.Client{
Timeout: 10 * time.Second,
}
// FetchImage downloads the OG image and returns the PNG bytes.
func FetchImage(p Params) ([]byte, error) {
resp, err := client.Get(BuildURL(p))
if err != nil {
return nil, fmt.Errorf("ogpeek: request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ogpeek: unexpected status %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("ogpeek: failed to read body: %w", err)
}
return data, nil
}
The url.Values type handles all percent-encoding automatically. The # in BrandColor becomes %23 in the final URL without any manual escaping. The shared http.Client reuses TCP connections via its internal transport pool, which is exactly what you want for repeated API calls. The defer resp.Body.Close() ensures the response body is always closed, preventing resource leaks even when errors occur.
Important: Always use a shared http.Client with a timeout. The zero-value http.DefaultClient has no timeout, which means a slow or unresponsive server will hang your goroutine indefinitely. A 10-second timeout is appropriate for image generation requests.
Gin Handler for Dynamic OG Images
Gin is the most popular Go web framework, and integrating OGPeek takes just a few lines. You can either proxy the image through your own domain or inject the OGPeek URL into your HTML templates for social media crawlers to fetch directly.
Proxy endpoint
This handler accepts query parameters, builds the OGPeek URL, fetches the image, and returns it with proper caching headers:
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"yourapp/ogpeek"
)
func main() {
r := gin.Default()
// Proxy OG image through your domain
r.GET("/og-image", func(c *gin.Context) {
title := c.DefaultQuery("title", "Untitled")
subtitle := c.Query("subtitle")
p := ogpeek.DefaultParams(title)
p.Subtitle = subtitle
data, err := ogpeek.FetchImage(p)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{
"error": "failed to generate OG image",
})
return
}
// Cache for 24 hours at CDN and browser level
c.Header("Cache-Control", "public, max-age=86400")
c.Data(http.StatusOK, "image/png", data)
})
r.Run(":8080")
}
Template middleware for og:image meta tags
For server-rendered pages, a middleware can inject the OGPeek URL into every request context so your HTML templates can reference it directly:
// OGPeekMiddleware injects an OG image URL into the Gin context
// for downstream handlers to use in HTML templates.
func OGPeekMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
title := c.GetString("pageTitle")
if title == "" {
title = "My Go App"
}
p := ogpeek.DefaultParams(title)
ogURL := ogpeek.BuildURL(p)
c.Set("ogImageURL", ogURL)
c.Next()
}
}
// In your blog handler:
func blogPostHandler(c *gin.Context) {
slug := c.Param("slug")
post, err := findPostBySlug(slug)
if err != nil {
c.HTML(http.StatusNotFound, "404.html", nil)
return
}
// Set page title before middleware reads it
p := ogpeek.DefaultParams(post.Title)
p.Subtitle = post.Author + " · " + post.ReadTime
c.HTML(http.StatusOK, "post.html", gin.H{
"Post": post,
"OGImageURL": ogpeek.BuildURL(p),
})
}
In your Go HTML template, reference the URL in your <head>:
<!-- templates/post.html -->
<head>
<title>{{ .Post.Title }}</title>
<meta property="og:title" content="{{ .Post.Title }}">
<meta property="og:image" content="{{ .OGImageURL }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{{ .OGImageURL }}">
</head>
Social media crawlers fetch the og:image URL directly from your HTML. Because the URL points to the OGPeek CDN, there is no additional load on your Go server. The crawlers get a cached PNG in milliseconds.
Echo Framework Integration
Echo is another widely-used Go web framework known for its performance and minimalist API. The integration pattern is similar to Gin but uses Echo’s context interface:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"yourapp/ogpeek"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Proxy OG image endpoint
e.GET("/og-image", handleOGImage)
// Blog routes with OG image URLs in templates
e.GET("/blog/:slug", handleBlogPost)
e.Logger.Fatal(e.Start(":8080"))
}
func handleOGImage(c echo.Context) error {
title := c.QueryParam("title")
if title == "" {
title = "Untitled"
}
p := ogpeek.DefaultParams(title)
p.Subtitle = c.QueryParam("subtitle")
data, err := ogpeek.FetchImage(p)
if err != nil {
return c.JSON(http.StatusBadGateway, map[string]string{
"error": "failed to generate OG image",
})
}
c.Response().Header().Set("Cache-Control", "public, max-age=86400")
return c.Blob(http.StatusOK, "image/png", data)
}
func handleBlogPost(c echo.Context) error {
slug := c.Param("slug")
post, err := findPostBySlug(slug)
if err != nil {
return c.String(http.StatusNotFound, "Post not found")
}
p := ogpeek.DefaultParams(post.Title)
p.Subtitle = post.Author
return c.Render(http.StatusOK, "post.html", map[string]interface{}{
"Post": post,
"OGImageURL": ogpeek.BuildURL(p),
})
}
Echo middleware approach
Echo’s middleware signature makes it straightforward to inject OG image URLs into every request:
// OGPeekMiddleware creates an Echo middleware that sets the OG image URL
// in the request context based on a title extraction function.
func OGPeekMiddleware(titleFn func(echo.Context) string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
title := titleFn(c)
if title != "" {
p := ogpeek.DefaultParams(title)
c.Set("ogImageURL", ogpeek.BuildURL(p))
}
return next(c)
}
}
}
// Usage:
// e.Use(OGPeekMiddleware(func(c echo.Context) string {
// return c.Param("slug")
// }))
The middleware accepts a function that extracts the title from the request context, making it reusable across different route groups. Blog routes might extract the title from a slug parameter, while product routes might extract it from a query parameter or database lookup.
Fiber Handler for OG Images
Fiber is built on top of fasthttp and is known for its Express-like API and raw performance. Here is a complete Fiber integration with both a proxy endpoint and template rendering:
package main
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"yourapp/ogpeek"
)
func main() {
engine := html.New("./views", ".html")
app := fiber.New(fiber.Config{
Views: engine,
})
// Proxy endpoint: returns the OG image as PNG
app.Get("/og-image", func(c *fiber.Ctx) error {
title := c.Query("title", "Untitled")
subtitle := c.Query("subtitle")
p := ogpeek.DefaultParams(title)
p.Subtitle = subtitle
data, err := ogpeek.FetchImage(p)
if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{
"error": "failed to generate OG image",
})
}
c.Set("Cache-Control", "public, max-age=86400")
c.Set("Content-Type", "image/png")
return c.Send(data)
})
// Blog post with OG image URL passed to template
app.Get("/blog/:slug", func(c *fiber.Ctx) error {
slug := c.Params("slug")
post, err := findPostBySlug(slug)
if err != nil {
return c.Status(fiber.StatusNotFound).SendString("Not found")
}
p := ogpeek.DefaultParams(post.Title)
p.Subtitle = post.Author + " · " + post.ReadTime
return c.Render("post", fiber.Map{
"Post": post,
"OGImageURL": ogpeek.BuildURL(p),
})
})
log.Fatal(app.Listen(":8080"))
}
Fiber’s API is deliberately similar to Express.js, which makes this code immediately readable for anyone coming from a Node.js background. The c.Send(data) method writes raw bytes to the response, and c.Set adds response headers. The template rendering works identically to the Gin and Echo examples—pass the OGPeek URL as a template variable and reference it in your og:image meta tag.
Direct URL vs. proxy: You do not have to proxy through your Go server. You can embed the OGPeek URL directly in your HTML template’s og:image meta tag. Proxying adds latency on the first request but gives you control over caching headers and keeps all URLs on your domain. For most applications, the direct URL approach is simpler and faster.
Concurrent Batch Generation with Goroutines
When you need to generate OG images for many items at once—migrating a blog to new preview images, pre-warming the CDN cache after a template change, or generating images for an entire product catalog—Go’s goroutines and channels make concurrent processing natural and efficient:
package main
import (
"fmt"
"os"
"path/filepath"
"sync"
"yourapp/ogpeek"
)
// BatchItem represents a single OG image to generate.
type BatchItem struct {
Params ogpeek.Params
Filename string
}
// BatchResult holds the outcome of a single generation attempt.
type BatchResult struct {
Filename string
Size int
Err error
}
// GenerateBatch processes multiple OG image requests concurrently
// using a semaphore channel to limit parallelism.
func GenerateBatch(items []BatchItem, outputDir string, maxConcurrency int) []BatchResult {
// Semaphore channel limits concurrent API requests
sem := make(chan struct{}, maxConcurrency)
results := make([]BatchResult, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
go func(idx int, it BatchItem) {
defer wg.Done()
// Acquire semaphore slot
sem <- struct{}{}
defer func() { <-sem }()
data, err := ogpeek.FetchImage(it.Params)
if err != nil {
results[idx] = BatchResult{
Filename: it.Filename,
Err: err,
}
return
}
outPath := filepath.Join(outputDir, it.Filename)
if err := os.WriteFile(outPath, data, 0644); err != nil {
results[idx] = BatchResult{
Filename: it.Filename,
Err: fmt.Errorf("write failed: %w", err),
}
return
}
results[idx] = BatchResult{
Filename: it.Filename,
Size: len(data),
}
}(i, item)
}
wg.Wait()
return results
}
func main() {
items := []BatchItem{
{ogpeek.Params{Title: "Understanding Goroutines", Subtitle: "Concurrency", Template: "gradient", Theme: "dark", BrandColor: "#F59E0B"}, "goroutines.png"},
{ogpeek.Params{Title: "Error Handling in Go", Subtitle: "Best Practices", Template: "gradient", Theme: "dark", BrandColor: "#F59E0B"}, "error-handling.png"},
{ogpeek.Params{Title: "Building REST APIs", Subtitle: "net/http", Template: "gradient", Theme: "dark", BrandColor: "#F59E0B"}, "rest-apis.png"},
{ogpeek.Params{Title: "Go Modules Explained", Subtitle: "Dependency Management", Template: "gradient", Theme: "dark", BrandColor: "#F59E0B"}, "modules.png"},
{ogpeek.Params{Title: "Testing in Go", Subtitle: "Table-Driven Tests", Template: "gradient", Theme: "dark", BrandColor: "#F59E0B"}, "testing.png"},
}
results := GenerateBatch(items, "./og-images", 3)
for _, r := range results {
if r.Err != nil {
fmt.Fprintf(os.Stderr, "FAIL %s: %v\n", r.Filename, r.Err)
} else {
fmt.Printf("OK %s (%d bytes)\n", r.Filename, r.Size)
}
}
}
The buffered channel sem acts as a counting semaphore. Each goroutine sends a value into the channel before making the API call and receives from it when done. If the channel is full (all slots occupied), the goroutine blocks until a slot opens. This limits concurrent HTTP requests to maxConcurrency without any external dependencies. The sync.WaitGroup ensures the function does not return until every goroutine has finished. Each result is written to its own index in the pre-allocated slice, so there is no contention or need for a mutex.
Rate limit awareness: The free tier allows 50 images per day. Even on paid plans, limit concurrency to 3–5 to be a good API citizen. The semaphore pattern shown above makes this trivial to configure.
CLI Tool with Cobra
Cobra is the standard library for building CLI applications in Go. Here is a complete command-line tool that generates OG images from flags, making it perfect for CI/CD pipelines, static site generators, and build scripts:
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"yourapp/ogpeek"
)
func main() {
var (
title string
subtitle string
template string
theme string
brandColor string
output string
)
rootCmd := &cobra.Command{
Use: "oggen",
Short: "Generate OG images using the OGPeek API",
Long: "A CLI tool that generates Open Graph preview images via the OGPeek API. Perfect for static site builds and CI/CD pipelines.",
RunE: func(cmd *cobra.Command, args []string) error {
if title == "" {
return fmt.Errorf("--title is required")
}
p := ogpeek.Params{
Title: title,
Subtitle: subtitle,
Template: template,
Theme: theme,
BrandColor: brandColor,
}
fmt.Fprintf(os.Stderr, "Generating OG image for %q...\n", title)
data, err := ogpeek.FetchImage(p)
if err != nil {
return fmt.Errorf("generation failed: %w", err)
}
if err := os.WriteFile(output, data, 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", output, err)
}
fmt.Fprintf(os.Stderr, "Saved %s (%d bytes)\n", output, len(data))
return nil
},
}
flags := rootCmd.Flags()
flags.StringVarP(&title, "title", "t", "", "Headline text (required)")
flags.StringVarP(&subtitle, "subtitle", "s", "", "Secondary text below title")
flags.StringVar(&template, "template", "gradient", "Visual template: gradient, minimal, split")
flags.StringVar(&theme, "theme", "dark", "Color scheme: dark, midnight, dawn, slate")
flags.StringVar(&brandColor, "brand-color", "#F59E0B", "Accent color as hex value")
flags.StringVarP(&output, "output", "o", "og-image.png", "Output file path")
_ = rootCmd.MarkFlagRequired("title")
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
Usage examples for your build pipeline:
# Generate a single OG image
$ oggen --title "Understanding Goroutines" --subtitle "Go Concurrency" -o public/og/goroutines.png
# Use in a shell loop for a static site
$ for post in content/blog/*.md; do
title=$(head -5 "$post" | grep "^title:" | cut -d'"' -f2)
slug=$(basename "$post" .md)
oggen -t "$title" -s "My Blog" -o "public/og/${slug}.png"
done
# Use in a Makefile target
og-images:
oggen -t "Home" -s "My App" -o public/og/home.png
oggen -t "About Us" -s "My App" -o public/og/about.png
oggen -t "Pricing" -s "Plans & Features" -o public/og/pricing.png
Cobra gives you flag parsing, help text generation, argument validation, and shell completion out of the box. The RunE variant returns an error instead of calling os.Exit directly, which makes the command testable. The MarkFlagRequired call ensures the user cannot forget the --title flag. Output goes to stderr for status messages and the file for the actual image, following Unix conventions.
Pricing and Rate Limits
OGPeek offers three tiers to match your usage:
- Free: 50 images per day. No API key required. Images include a small watermark. Perfect for prototyping, personal projects, and trying the API before committing.
- Starter ($9/month): 10,000 images per month. No watermark. Priority CDN caching. Ideal for blogs, portfolios, and small SaaS applications.
- Pro ($29/month): 50,000 images per month. No watermark. Custom templates. Webhook notifications. Built for high-traffic platforms and agencies.
All tiers use the same API endpoint and the same parameters. Upgrading is a matter of adding your API key to the request—no code changes beyond that. The concurrency patterns shown in this guide (semaphore channels, rate limiting) work well with any tier to ensure you stay within your plan’s limits.
Try OGPeek free — 50 images/day, no API key
Generate dynamic Open Graph images for your Go app in minutes. No signup required.
View pricing →Wrapping Up
Go gives you multiple clean integration points for OGPeek depending on your stack and use case:
- net/http client: A reusable package with
BuildURLandFetchImagefunctions that handle URL encoding, HTTP requests, error handling, and connection pooling. This is the foundation every other pattern builds on. - Gin handlers: Proxy endpoints with
c.Datafor serving images through your domain, and template integration withc.HTMLfor injecting OG image URLs into your HTML<head>. - Echo handlers: The same proxy and template patterns using Echo’s
c.Blobandc.Rendermethods, plus a reusable middleware that accepts a title extraction function. - Fiber handlers: Express-like API with
c.Sendfor raw bytes and template rendering for server-side pages. Fiber’s fasthttp foundation means minimal overhead per request. - Goroutine batch processing: A semaphore channel pattern for controlled concurrency when generating images for an entire blog, product catalog, or content migration.
- Cobra CLI tool: A standalone binary for CI/CD pipelines, static site generators, and Makefiles that generates OG images from command-line flags.
The underlying principle is the same regardless of which pattern you choose: build a URL with your parameters, point it at the OGPeek API, and let the CDN handle image generation and caching. Your Go code does zero image processing, imports zero native image libraries, and manages zero image files on disk.
For more on the OGPeek API, see the complete API reference. For framework-specific guides, check out C# + OGPeek, Kotlin + OGPeek, or Next.js + OGPeek.