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.
git clone https://github.com/iwant2facilitate/facilitation-value-tool
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"
.agents/skills/custom-link-card/SKILL.mdCustom 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
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)
server/vite.tsAfter
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)
server/static.tsTwo critical changes:
-
Disable automatic index serving:
— without this,express.static(distPath, { index: false })
servesexpress.static
directly with Replit's injected tags, bypassing the replacement.index.html -
Catch-all route reads and replaces: Read
from disk, replace OG tags with absolute URLs, then send.index.html
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
serving index.html: Withoutexpress.static
, the static middleware serves the built{ index: false }
(with Replit's default tags) before the catch-all route runs.index.html- 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"