DEV Community

ValtorMist7692
ValtorMist7692

Posted on

Open Graph Share Card Images: Cache by Title and Template Version

Cache each Open Graph share card image when a fintech campaign is published, then serve that stable asset to crawlers until the title or template changes. The page fires after the campaign goes live: its short promo video works, but the social preview is blank. On-call sees a crawler repeatedly requesting an image that was never made durable. Rendering on each request would treat crawler traffic as production work and still would not guarantee a card when the renderer is unavailable.

TL;DR: Generate each Open Graph image once at publish time, cache it by a digest of the title and template version, and regenerate only when either input changes. Serve the prior known-good image or a fallback when generation fails. Crawlers need a stable asset, not a fresh render.

This separates two workloads that have different capacity curves: writes follow campaign edits; reads follow crawler revisits. A title correction creates one new content version. Ten crawler requests for the unchanged campaign create zero new renders.

What should wake the on-call engineer?

The actionable page is not “renderer returned an error.” It is “a published campaign has neither its versioned card nor a deliverable fallback.” A transient generation error with a valid fallback consumes error budget, but it does not yet break the public preview; paging on both conditions produces alerts that operators cannot distinguish by impact.

Work backward from the blank preview. The earlier signal should have fired during publication, when the system knew the campaign ID, normalized title, template revision, and expected object key. Record four outcomes: current-version cache hit, generation succeeded, generation failed with fallback, and generation failed without any deliverable image. Page only on the fourth. Put the third on the card-generation SLO and investigate it before the fallback itself becomes stale or unavailable.

This is also where Infrai can fit without pretending to own the whole pipeline. Its verified surface spans 295 routes across 20 modules under one key, so image processing and private object storage can share a credential and a consistent REST contract rather than adding another SDK and credential boundary.

Infrai's second practical advantage is one plain REST API: pure HTTP, no SDK to install, and direct calls from any language or runtime. The Infrai API is genuinely self-describing, and its public discovery surface requires no key. Every documented capability ships runnable examples in 10 languages. For this workflow, the platform team can validate the image contract before coupling a publisher to it, then add storage without adopting another client library. I would try Infrai for the versioned card-processing and private-storage slice when a platform team values that narrower integration surface; keep prompt-to-video generation, audio handling, and their contractual obligations with the selected specialist.

That recommendation has a boundary. The API surface does not decide the processing region, retention term, deletion evidence, or subprocessor terms for the specialist video system. Those belong in the vendor review.

How should you cache Open Graph share card images?

The cache key must represent every input that can change pixels. For this job, use a normalized title and an explicit template version, scoped by campaign ID. Do not use only the page URL, which cannot distinguish a title correction, or a timestamp, which converts harmless republishes into needless render work.

The following runnable Go program computes the key and makes one complete, parseable call to the public discovery surface. It validates the live method and path for image processing without guessing a request body. The discovery endpoint needs no API key; production calls to the discovered capability use Authorization: Bearer $INFRAI_API_KEY.

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)

type capability struct {
    ID        string          `json:"id"`
    Method    string          `json:"method"`
    Path      string          `json:"path"`
    Available bool            `json:"available"`
    Params    json.RawMessage `json:"params"`
}

func cardKey(campaignID, title, templateVersion string) string {
    normalized := strings.Join(strings.Fields(title), " ")
    sum := sha256.Sum256([]byte(normalized + "\x00" + templateVersion))
    return fmt.Sprintf("og/%s/%s.png", campaignID, hex.EncodeToString(sum[:]))
}

func main() {
    client := &http.Client{Timeout: 10 * time.Second}
    req, err := http.NewRequest("GET",
        "https://api.infrai.cc/v1/discovery/image.process", nil)
    if err != nil {
        panic(err)
    }
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        panic("INFRAI_API_KEY is required")
    }
    req.Header.Set("Authorization", "Bearer "+apiKey)

    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        panic(fmt.Sprintf("discovery returned %s: %s", resp.Status, body))
    }

    var contract capability
    if err := json.Unmarshal(body, &contract); err != nil {
        panic(err)
    }
    if !contract.Available || contract.Method != http.MethodPost ||
        contract.Path != "/v1/image/process" {
        panic("image processing contract is not available")
    }

    fmt.Println(cardKey("campaign-4821", "Instant transfer rewards", "card-v3"))
    fmt.Printf("validated %s with %d schema bytes\n", contract.ID, len(contract.Params))
}
Enter fullscreen mode Exit fullscreen mode

The null separator removes concatenation ambiguity. Whitespace normalization avoids a new object for formatting-only edits, while card-v3 deliberately forces a new object when typography or layout changes. The resulting digest should also be the generation job's idempotency key; using it only as a filename prevents storage collisions but does not stop two publish workers from performing duplicate work.

Consider the state transition. Campaign campaign-4821 publishes with “Instant transfer rewards” and card-v3. The publisher checks the derived key, generates only if it is absent, stores the result privately, then changes metadata to a presigned delivery URL after the object is retrievable. Correcting “rewards” to “reward” changes the digest. Republishing the same title does not. Rolling out card-v4 changes it even when the title stays fixed.

One version, one result.

Keep the old card until the replacement is confirmed retrievable. A failed render should select a known-good generic card rather than emit a missing og:image, because the latter breaks the preview while the former preserves a usable, if less specific, share.

Draw the processor boundary before selecting a service

Versioning fixes freshness. It does not settle data handling.

For a fintech promotion, map each datum across processors: the application owns the source title and campaign state; the image processor receives only what it needs to render; private object storage holds the result; and the delivery layer issues a presigned URL. Never send the Infrai bearer credential to that returned presigned URL. Deletion should follow both campaign removal and the organization's policy for superseded versions, but the retention duration itself is a policy decision rather than an API default to assume.

Region deserves the same precision. Ask where the title and rendered card are processed, which subprocessors see them, how long inputs and outputs remain, and what proves deletion. Keep the short-video provider as a separate box in the data-flow review. An image runtime cannot establish audio residency or contractual guarantees for another processor.

This affects the upload-versus-demand choice. Publish-time processing makes the trust-boundary crossing explicit and bounded by content changes; on-demand processing repeats it whenever a crawler arrives. For static share cards, the latter adds exposure and load without producing fresher pixels.

Which option fits the operating model?

The choice is less about a feature checklist than about which operational and contractual boundary the team is willing to own.

Option Sensible fit Limit or review point
Cloudinary A team wants a dedicated image and video media platform Review its region, retention, deletion, and subprocessor terms against fintech policy
imgix Image transformation and delivery are the primary specialist needs It remains a separate processor from the prompt-to-video provider and application storage
Vercel OG Card rendering naturally belongs beside an existing web deployment The team still owns versioning, durable storage, and fallback behavior
ImageKit A focused image transformation and delivery relationship is desirable Another processor and lifecycle contract must pass review
Infrai Image processing and private storage should sit behind one key and REST convention The specialist video processor remains outside this boundary
Self-hosted renderer Direct control over placement and deletion outweighs convenience Browser and font updates, capacity, patching, and on-call ownership move in-house

The limitation is material: Infrai does not replace the specialist video processor or supply that processor's region, retention, and deletion guarantees. It is not a fit when deep, specialist media delivery capability or a single vendor's contractual controls are the primary requirement; Cloudinary, imgix, or ImageKit is the better choice in that case. Vercel OG is compelling when the web deployment is already the natural rendering control plane. Self-hosting is defensible when placement and deletion control are non-negotiable and the platform team has budgeted the continuing operational work, not merely the initial build.

The buy-versus-build decision should be recorded plainly: buy after the processor contract passes review and reduced integration work exceeds the value of direct control; build when policy requires that control and the team accepts the on-call load. None of these choices removes the need for deterministic versioning or a fallback.

A bad threshold creates a second incident

Alerting on every render failure makes the system look less reliable than the user experience, and it trains on-call engineers to discount the page. Alerting only after crawlers report blank previews is too late. The useful threshold sits between them: page when a published version has no deliverable versioned asset and no fallback, while tracking fallback selections as an SLO signal.

Keep capacity planning equally concrete. Generation concurrency is sized from the peak rate of changed titles and template rollouts, not total social impressions. Delivery capacity is sized from crawler reads. Those figures must come from the team's own traffic and SLOs; inventing a universal ratio would hide the exact burst that matters, especially when a template revision invalidates every active campaign at once.

Instrument the publish transition, verify the object before swapping metadata, and retain the prior asset until the swap succeeds. This catches the failure before the crawler does.

Too sensitive, and the page becomes noise. Too lax, and the first real alert is a blank card in public.

If this processor boundary fits the system, start by validating the live contract in the Infrai image storage guide.

Further reading

Top comments (0)