Facilitation-value-tool custom-link-card

Create custom Open Graph link preview cards for social media and text messages. Use when the user asks to create or update a link card, link preview, OG image, or social sharing image.

install
source · Clone the upstream repo
git clone https://github.com/iwant2facilitate/facilitation-value-tool
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/iwant2facilitate/facilitation-value-tool "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.agents/skills/custom-link-card" ~/.claude/skills/iwant2facilitate-facilitation-value-tool-custom-link-card && rm -rf "$T"
manifest: .agents/skills/custom-link-card/SKILL.md
source content

Custom Link Card (OG Image)

Create a custom Open Graph image and configure the server to serve it correctly for social media crawlers and text message previews (iMessage, Facebook, Slack, LinkedIn, etc.).

Overview

Replit's build process injects its own default OG meta tags (pointing to

opengraph.png
or
opengraph.jpg
) into the built
index.html
, overriding whatever is in the source. The solution is to dynamically replace the OG tags at runtime on the server side.

Steps

1. Create the OG Image (SVG → PNG)

Create an SVG at

/tmp/og-image.svg
(1200×630 viewport) with the desired text/branding, then convert:

convert /tmp/og-image.svg -resize 1200x630 client/public/og-image.png

The image lands in

client/public/
so Vite includes it in the build output.

2. Set Meta Tags in
client/index.html

<meta property="og:image" content="/og-image.png" />
<meta name="twitter:image" content="/og-image.png" />

These will be overridden by Replit's build, but serve as the source-of-truth for development.

3. Server-Side Tag Replacement (Critical)

Replit's build replaces OG tags with its defaults. The server must dynamically fix them at runtime.

server/vite.ts
(Development)

After

vite.transformIndexHtml()
, replace OG tags before sending:

const protocol = req.headers['x-forwarded-proto'] || 'https';
const host = req.headers['x-forwarded-host'] || req.headers.host || '';
const origin = `${protocol}://${host}`;
let finalPage = page.replace(/<meta property="og:image"[^>]*>/,
  `<meta property="og:image" content="${origin}/og-image.png" />`);
finalPage = finalPage.replace(/<meta name="twitter:image"[^>]*>/,
  `<meta name="twitter:image" content="${origin}/og-image.png" />`);

server/static.ts
(Production)

Two critical changes:

  1. Disable automatic index serving:

    express.static(distPath, { index: false })
    — without this,
    express.static
    serves
    index.html
    directly with Replit's injected tags, bypassing the replacement.

  2. Catch-all route reads and replaces: Read

    index.html
    from disk, replace OG tags with absolute URLs, then send.

app.use(express.static(distPath, { index: false }));

app.use("/{*path}", (req, res) => {
  const indexPath = path.resolve(distPath, "index.html");
  let html = fs.readFileSync(indexPath, "utf-8");
  const protocol = req.headers['x-forwarded-proto'] || 'https';
  const host = req.headers['x-forwarded-host'] || req.headers.host || '';
  const origin = `${protocol}://${host}`;
  html = html.replace(/<meta property="og:image"[^>]*>/,
    `<meta property="og:image" content="${origin}/og-image.png" />`);
  html = html.replace(/<meta name="twitter:image"[^>]*>/,
    `<meta name="twitter:image" content="${origin}/og-image.png" />`);
  // Remove Replit-injected size/type tags
  html = html.replace(/<meta property="og:image:width"[^>]*>\s*/g, '');
  html = html.replace(/<meta property="og:image:height"[^>]*>\s*/g, '');
  html = html.replace(/<meta property="og:image:type"[^>]*>\s*/g, '');
  res.status(200).set({ "Content-Type": "text/html" }).end(html);
});

Key Regex Pattern

Use

[^>]*>
not
[^"]*" \/>
— Replit injects tags in varying formats (
opengraph.jpg
,
opengraph.png
, with or without self-closing slash). The broad pattern catches all variants.

Common Pitfalls

  • express.static
    serving index.html
    : Without
    { index: false }
    , the static middleware serves the built
    index.html
    (with Replit's default tags) before the catch-all route runs.
  • Relative URLs: Social media crawlers require absolute URLs with full
    https://domain.com/og-image.png
    .
  • Caching: Platforms like iMessage, Slack, Facebook cache link previews aggressively. Users must paste links in new conversations to see updates. Facebook has a Sharing Debugger to clear cache.

Verification

# Check deployed tags
curl -sL "https://YOUR-APP.replit.app/" | grep -i "og:image\|twitter:image"

# Check image is accessible
curl -sL -o /dev/null -w "%{http_code} %{content_type}" "https://YOUR-APP.replit.app/og-image.png"