Feature spec
BeginnerCategory
communication-social
Build with AI
2–4 hours with Lovable or V0
Custom build
2–3 days custom dev
Running cost
$0/mo up to 1K users · $25–50/mo at scale
Works on
Everything it takes to ship Content Sharing — parts, prompts, and real costs.
Content sharing needs four pieces: a share button using navigator.share() with clipboard fallback, a dynamic OG image generated per content item at 1200x630px, a Supabase shares table recording which platform each share went to, and a deep link resolver for mobile that opens the correct in-app screen. With V0 or Lovable you can ship a working share button and OG image in 2–4 hours for $0/month — Vercel OG generation is included in the Hobby plan.
What Content Sharing Actually Is
Content sharing gives users a one-tap way to spread your app's content — posts, products, recipes, profiles — to social media, messaging apps, or other users inside the app. The feature has two visible parts (the share button and the share count) and two invisible parts that determine whether shared links actually work: the OG metadata that produces a preview card in WhatsApp and iMessage, and the deep link resolver that opens the correct in-app screen when a mobile user taps the shared URL. Most AI-generated share buttons skip both invisible parts — resulting in links that share a bare URL with no preview and open the app homepage instead of the shared content.
What users consider table stakes in 2026
- One-tap share button producing a link with a preview image, title, and description when pasted into WhatsApp, iMessage, Twitter, or Slack
- Native share sheet on mobile browsers (iOS Safari, Chrome Android) — not a custom modal
- Copy-to-clipboard fallback for desktop browsers where the Web Share API is unavailable
- Share count visible on content cards, incrementing immediately when the user shares (optimistic update)
- Deep link that resolves to the correct content screen in the mobile app, not the app homepage
- Share tracking so creators can see which platform drove the most traffic to their content
Anatomy of the Feature
Six components. The OG metadata generator and the share event tracker are the two components AI tools most often omit or implement incompletely. The deep link resolver is optional but critical for mobile apps.
Share button and counter
UIA shadcn/ui Button with a ShareIcon from Lucide. The share count is fetched from the Supabase shares table (COUNT grouped by content_id) and displayed next to the icon. On click, the share count increments optimistically by 1 before the network call completes — the actual count from the server confirms or corrects it on response.
Note: For share counts above 1,000, display as '1.2K' not '1,200' — abbreviated counts read faster on content cards with many items.
Web Share API handler
UICalls navigator.share({ title, text, url }) if the browser supports it — detected at runtime with `if (navigator.share)`. The share sheet is presented natively by the OS: iOS shows AirDrop + Messages + social apps; Android shows its standard share targets. If navigator.share is undefined (desktop Chrome, Firefox), falls back to copying the URL to the clipboard with navigator.clipboard.writeText() and showing a 'Link copied!' toast.
Note: navigator.share() requires the page to be served over HTTPS and have focus — it fails inside Lovable's preview iframe. Always test on the published URL.
OG metadata generator
BackendA Next.js generateMetadata() async function on each content route that fetches the content row from Supabase by its ID and returns an OpenGraph object: { title: content.title, description: content.summary, images: [{ url: `/api/og?id=${content.id}`, width: 1200, height: 630 }] }. This makes every shared link render a preview card on every platform that reads OG tags.
Note: generateMetadata runs server-side on every request — make sure the Supabase fetch is cached with Next.js fetch cache or React cache() to avoid a DB hit per social media crawler.
Dynamic OG image
ServiceA Next.js Edge Runtime route at /api/og that uses @vercel/og (ImageResponse from the 'next/og' import) to generate a branded 1200x630 PNG image from JSX. The image includes the content title, author name, content category, and your app's logo on a branded background. The content_id route param fetches the relevant data from Supabase.
Note: Set cache headers on the OG image response: Cache-Control: public, max-age=3600, s-maxage=86400. For content that updates frequently, add a cache-busting query param: ?v={updated_at_unix_timestamp}.
Share event tracker
DataA Supabase shares table records every share event: content_id, content_type, shared_by (uuid, nullable for unauthenticated users), platform (twitter/whatsapp/copy/native), referrer (the URL the share was opened from), and created_at. Inserted via a Supabase Server Action or Edge Function on share click.
Note: Insert the share event before calling navigator.share() so the count increments even if the user dismisses the native share sheet without completing the share.
Deep link resolver
BackendFor mobile apps: Branch.io or Firebase Dynamic Links maps a shared web URL (yourapp.com/content/:id) to the correct in-app screen. When the app is installed, the link opens the app directly to the content. When the app is not installed, it opens the App Store/Play Store and then the correct screen after install (deferred deep link).
Note: Deep links are only needed if you also have a mobile app. For web-only apps, standard Next.js routing on the shared URL is sufficient.
The data model
One table tracks all share events. The content table itself needs OG metadata columns (or they're generated on the fly). Run this in the Supabase SQL Editor.
1-- Shares tracking table2create table public.shares (3 id uuid primary key default gen_random_uuid(),4 content_id uuid not null,5 content_type text not null default 'post',6 shared_by uuid references auth.users(id) on delete set null,7 platform text not null default 'copy' check (platform in ('twitter', 'whatsapp', 'facebook', 'linkedin', 'copy', 'native', 'other')),8 referrer text,9 created_at timestamptz not null default now()10);1112create index shares_content_idx on public.shares (content_id, created_at desc);13create index shares_shared_by_idx on public.shares (shared_by) where shared_by is not null;14create index shares_platform_idx on public.shares (platform, created_at desc);1516alter table public.shares enable row level security;1718-- Anyone can insert a share (including unauthenticated users)19create policy "Anyone can insert shares"20 on public.shares for insert21 with check (true);2223-- Content creators can view shares of their content24-- (adjust content_id join to match your content table)25create policy "Creators can view their content shares"26 on public.shares for select27 using (28 shared_by = auth.uid()29 or exists (30 select 1 from public.posts31 where posts.id = shares.content_id32 and posts.author_id = auth.uid()33 )34 );3536-- Add OG columns to your existing content table if not already present37-- alter table public.posts add column if not exists og_title text;38-- alter table public.posts add column if not exists og_description text;39-- alter table public.posts add column if not exists og_image_url text;40-- (or generate OG metadata on the fly from existing title/body/thumbnail columns)4142-- Materialized share counts for content cards43create materialized view public.share_counts as44 select content_id, count(*) as total45 from public.shares46 group by content_id;4748create unique index share_counts_content_idx on public.share_counts (content_id);4950-- Refresh the view periodically (call this from a Supabase cron or Edge Function)51-- select cron.schedule('refresh-share-counts', '*/5 * * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY public.share_counts');Heads up: The platform column accepts 'native' for cases where navigator.share() was called but the user chose a target not tracked individually. The materialized view for share counts avoids a COUNT query per content card — refresh it every 5 minutes via a Supabase Edge Function cron job rather than on every page load.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Best path for web — V0 generates the Next.js generateMetadata() function and @vercel/og image route natively. Vercel deployment handles Edge Runtime OG image generation without any additional setup.
Step by step
- 1Prompt V0 with the share feature spec below — it generates the share button component, generateMetadata route, and /api/og image route
- 2Set environment variables in V0's Vars panel: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY for the DB, plus SUPABASE_SERVICE_ROLE_KEY for server-side metadata fetching
- 3Run the shares table SQL from the data model section in your Supabase SQL Editor
- 4Deploy to Vercel — the OG image route uses Edge Runtime which only works on Vercel (or equivalent); test the OG preview at developers.facebook.com/tools/debug after deploying
Build a content sharing feature for a Next.js App Router app using Supabase. The content lives in a 'posts' table (id uuid, title text, body text, author_id uuid, thumbnail_url text, created_at timestamptz). Feature 1: ShareButton client component — calls navigator.share({ title: post.title, text: post.body.slice(0, 100), url: window.location.href }) if navigator.share is defined; otherwise calls navigator.clipboard.writeText(url) and shows a 'Link copied!' toast via shadcn/ui Toaster. After initiating share, insert a row to the 'shares' Supabase table (content_id, content_type: 'post', shared_by: current user id or null if not logged in, platform: 'native' for share API or 'copy' for clipboard). Show optimistic share count next to share icon — increment immediately, confirm from server response. Feature 2: generateMetadata() on app/posts/[id]/page.tsx — async function that fetches the post from Supabase by id and returns OpenGraph metadata: { title: post.title, description: post.body.slice(0, 155), openGraph: { title, description, images: [{ url: '/api/og?id=' + id + '&v=' + post.updated_at_unix, width: 1200, height: 630 }] } }. Feature 3: app/api/og/route.ts — Edge Runtime route using ImageResponse from 'next/og'; fetches post title, author name, and thumbnail_url from Supabase using the id query param; renders a branded 1200x630 image with: app logo top-left, content thumbnail right half, title text large center-left, author name small below title, category badge. Set Cache-Control: public, max-age=3600 on the response. Rate limit the shares insert endpoint: reject more than 5 shares per IP per minute using a counter stored in a Supabase Edge Function.Where this path bites
- No mobile native share sheet — this is a web-only path; add Branch.io for mobile deep links if you also have a native app
- Manual Supabase setup required — V0 does not auto-provision the database
- V0 occasionally generates hard-coded og:title instead of fetching dynamically — verify generateMetadata fetches from Supabase, not a static string
Third-party services you'll need
Three services power content sharing. @vercel/og and Supabase cover the core feature at no cost for small apps.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| @vercel/og | Dynamic OG image generation at /api/og — renders a branded 1200x630 preview image per content item using JSX and Edge Runtime | Free, included with Vercel Hobby and Pro plans | $0 on Hobby and Pro |
| Branch.io | Mobile deep linking — maps shared web URLs to in-app screens, handles deferred deep links for users who don't have the app installed yet | Free up to 10,000 monthly active users | From approx $59/mo for advanced attribution analytics |
| Supabase | Shares tracking table (content_id, platform, created_at), share counts materialized view, RLS for author-only analytics access | Free: 2 projects, 500MB DB | Pro: $25/mo (8GB DB) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
All free tiers cover this scale. Vercel Hobby handles OG image generation. Supabase free tier covers the shares table. Branch.io free tier covers deep links.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
navigator.share() silently fails in Lovable preview and V0 sandbox
Symptom: navigator.share() requires the page to have user focus and be served from a top-level browsing context. Lovable's preview runs inside a sandboxed iframe that blocks the Web Share API. Similarly, V0's sandbox preview cannot invoke native share. The share button appears to do nothing, which makes the feature look broken during development.
Fix: Always test the share button on the published/deployed URL, not in any preview pane. In Lovable: click Publish → Update, then open the live URL on your phone. In V0: deploy to Vercel first. The feature works correctly in production on HTTPS.
OG image not updating after content edits
Symptom: Social platforms and messaging apps cache OG images aggressively. Once WhatsApp or Slack fetches the /api/og?id=X image, it caches it for hours or days. Editing the post title or thumbnail does not automatically invalidate the cached OG image — sharers continue seeing the old preview.
Fix: Add a cache-busting query param to the OG image URL: ?v={updated_at_unix_timestamp}. When the post is updated, updated_at changes, which changes the image URL, which busts the cache. For immediate invalidation after a content update, use Vercel's CDN Purge API or set Cache-Control: max-age=60 for frequently changing content.
Share count inflates from crawlers and bots
Symptom: Social media platforms, search engine crawlers, and link preview bots all load the shared URL. If the shares insert happens server-side on every page load rather than on an explicit user action, bots trigger share inserts constantly. Share counts can reach thousands with zero real human shares.
Fix: Insert to the shares table only on explicit user action (the share button click), never on page load. Add IP-based rate limiting: reject more than 5 share inserts per IP per minute via a Supabase Edge Function or Vercel middleware using Upstash Redis as the rate limit state store.
Deep link opens the app homepage instead of the shared content
Symptom: Branch.io and Firebase Dynamic Links require explicit link pattern configuration mapping URL paths to in-app routes. Without this, the OS opens the app but the deep link handler doesn't know which screen to navigate to — it falls back to the home screen. Users who tap a shared playlist or article link land on the app's main page with no context.
Fix: In Branch.io Dashboard → Link Settings, configure routing rules: map /posts/:id to the PostDetail screen and pass the id as a link param. Test with the Branch.io mobile tester app before releasing — verify both the already-installed path and the fresh-install deferred path.
V0 generates static OG metadata missing dynamic content
Symptom: V0 sometimes generates generateMetadata() with hard-coded og:title values or with a simple static export instead of fetching from Supabase. Shared links show the app name and a generic description instead of the specific content title and image.
Fix: Include explicit instructions in your V0 prompt: 'generateMetadata must be an async function that fetches the post title, body, and thumbnail_url from Supabase using the id route param. Do not hardcode any og: values.' Verify the generated code: if generateMetadata is not async or does not contain a Supabase fetch call, it is generating static metadata.
Best practices
Always detect navigator.share() before calling it — implement the clipboard fallback for desktop browsers where the API is unavailable
Generate OG images using Edge Runtime (not Node.js runtime) for fast cold starts — OG images are fetched by crawlers worldwide and slow generation hurts share preview load time
Add a cache-busting version param to OG image URLs tied to content's updated_at timestamp — prevents stale previews after edits
Track share platform in the shares table (twitter/whatsapp/copy/native) — without platform attribution you can't tell which channels drive the most re-shares
Allow unauthenticated users to share (shared_by = null) — requiring login to share content kills virality, especially for first-time visitors
Rate limit share inserts at 5 per IP per minute to prevent count manipulation by motivated bad actors
Show share counts in abbreviated format above 999: '1.2K' rather than '1,234' — saves space on content cards and reads more naturally
When You Need Custom Development
The core share button and OG image are a standard AI-tool build. These requirements need custom development:
- Share revenue attribution — tracking which specific shares led to paid conversions, not just traffic, requires UTM parameter injection, referral tracking tables, and attribution modeling
- A/B testing OG image variants per content type — running two different image layouts and measuring which drives more click-throughs requires a testing framework and variant assignment logic
- Viral loop analytics dashboard with K-factor calculation — tracking share events, referral visits, and new sign-ups attributed to each share to compute the viral coefficient
- Branded short URL domain (share.yourapp.com) with redirect analytics and QR code generation per shared link
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
How do I add a share count to content cards?
Fetch the count from the Supabase share_counts materialized view (or a COUNT query on the shares table) for each content_id. Display it next to the share icon. For performance on feeds with many items, include the share count in the initial posts query using a Supabase join or embed it in the posts table as a denormalized counter column that increments via a trigger.
What's the difference between the Web Share API and a custom share modal?
The Web Share API (navigator.share()) invokes the device's native OS share sheet — the same one that appears when you tap Share in Safari or Chrome. It surfaces all the sharing targets the user has installed (WhatsApp, Messages, Copy Link, etc.) without you needing to build them. A custom share modal is a set of individual platform buttons you build yourself — more visual control, less coverage. Use the Web Share API first; fall back to a custom modal only if you need specific platform UTM tracking that navigator.share() can't provide.
How do I create a custom OG image for each piece of content?
Use @vercel/og (next/og) in a Next.js Edge Runtime route at /api/og?id=:contentId. The route fetches the content title, author name, and thumbnail from Supabase, then renders a JSX layout as a 1200x630 PNG. Reference this URL in your generateMetadata() function's openGraph.images array. The image is cached by Vercel CDN and served globally with low latency.
How do I track which platform users shared to?
navigator.share() does not report back which target the user chose — the browser invokes the native share sheet and you get no callback indicating Twitter vs WhatsApp vs Messages. The practical workaround: insert a share record with platform='native' for Web Share API calls, and platform='copy' for clipboard copies. For specific platform tracking, build individual buttons (Twitter, WhatsApp, LinkedIn) that construct platform-specific share URLs with UTM params — but this bypasses the native share sheet.
Can unregistered users share content?
Yes — and they should be able to. Set shared_by to null for unauthenticated shares. The RLS INSERT policy should allow `with check (true)` so anonymous users can write share events without authentication. Virality depends on anonymous sharing — requiring login before sharing is a common conversion-killing mistake.
How do I make shared links preview correctly in WhatsApp and iMessage?
Both WhatsApp and iMessage read OG meta tags (og:title, og:description, og:image) from the shared URL server-side before rendering the preview card. Your page must return these tags in the HTML on the initial server render — client-side rendering (SPA) doesn't work because crawlers don't execute JavaScript. Next.js generateMetadata() generates the correct server-rendered OG tags automatically. Test your previews at metatags.io or by pasting a URL directly into WhatsApp.
How do I prevent share counts from being gamed?
Rate limit share inserts: 5 shares per IP per minute using Upstash Redis in a Supabase Edge Function. For more robust protection, require a JavaScript challenge (Cloudflare Turnstile) before a share insert is accepted — bots can't pass the challenge. Also exclude your own team's IPs and known crawler user-agent strings from share counts.
How do I add UTM parameters to shared links?
Append UTM parameters to the shared URL before passing it to navigator.share(): ?utm_source=native_share&utm_medium=share&utm_campaign=content&utm_content={content_id}. For specific platform buttons: ?utm_source=twitter or ?utm_source=whatsapp. These params appear in your analytics tool (Google Analytics, PostHog, Mixpanel) and let you attribute traffic and conversions back to specific share events and platforms.
Need this feature production-ready?
RapidDev builds content sharing into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.