# How to Add Content Sharing to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

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.

## 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** (ui): A 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.
- **Web Share API handler** (ui): Calls 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.
- **OG metadata generator** (backend): A 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.
- **Dynamic OG image** (service): A 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.
- **Share event tracker** (data): A 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.
- **Deep link resolver** (backend): For 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).

## 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.

```sql
-- Shares tracking table
create table public.shares (
  id uuid primary key default gen_random_uuid(),
  content_id uuid not null,
  content_type text not null default 'post',
  shared_by uuid references auth.users(id) on delete set null,
  platform text not null default 'copy' check (platform in ('twitter', 'whatsapp', 'facebook', 'linkedin', 'copy', 'native', 'other')),
  referrer text,
  created_at timestamptz not null default now()
);

create index shares_content_idx on public.shares (content_id, created_at desc);
create index shares_shared_by_idx on public.shares (shared_by) where shared_by is not null;
create index shares_platform_idx on public.shares (platform, created_at desc);

alter table public.shares enable row level security;

-- Anyone can insert a share (including unauthenticated users)
create policy "Anyone can insert shares"
  on public.shares for insert
  with check (true);

-- Content creators can view shares of their content
-- (adjust content_id join to match your content table)
create policy "Creators can view their content shares"
  on public.shares for select
  using (
    shared_by = auth.uid()
    or exists (
      select 1 from public.posts
      where posts.id = shares.content_id
        and posts.author_id = auth.uid()
    )
  );

-- Add OG columns to your existing content table if not already present
-- alter table public.posts add column if not exists og_title text;
-- alter table public.posts add column if not exists og_description text;
-- alter table public.posts add column if not exists og_image_url text;
-- (or generate OG metadata on the fly from existing title/body/thumbnail columns)

-- Materialized share counts for content cards
create materialized view public.share_counts as
  select content_id, count(*) as total
  from public.shares
  group by content_id;

create unique index share_counts_content_idx on public.share_counts (content_id);

-- Refresh the view periodically (call this from a Supabase cron or Edge Function)
-- select cron.schedule('refresh-share-counts', '*/5 * * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY public.share_counts');
```

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 paths

### V0 — fit 5/10, 2–4 hours

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.

1. Prompt V0 with the share feature spec below — it generates the share button component, generateMetadata route, and /api/og image route
2. Set 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
3. Run the shares table SQL from the data model section in your Supabase SQL Editor
4. Deploy 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

Starter prompt:

```
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.
```

Limitations:

- 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

### Lovable — fit 4/10, 2–4 hours

Good path for the share button and share logging — Lovable auto-wires Supabase. OG image generation needs a second prompt targeting a Supabase Edge Function setup.

1. Create a new Lovable project with Lovable Cloud connected — Supabase Auth and database are provisioned automatically
2. Paste the prompt below — Lovable generates the ShareButton component and shares table logging in one shot
3. Add a second prompt: 'Create a Supabase Edge Function at /functions/og-image that accepts an id query param, fetches the post from Supabase, and returns a 1200x630 PNG using the Satori library with the post title, author, and thumbnail embedded in a branded layout'
4. Add your Resend or OG image domain in Cloud tab → Secrets if needed
5. Click Publish → Update before testing navigator.share() — it is blocked in the Lovable preview iframe

Starter prompt:

```
Build a content sharing feature in this Lovable project. The app has a 'posts' table in Supabase (id, title, body, author_id, thumbnail_url, created_at). Add a ShareButton component that: 1. Detects navigator.share support with if (navigator.share). 2. If supported: calls navigator.share({ title: post.title, text: post.body.slice(0,100), url: window.location.origin + '/posts/' + post.id }) and catches AbortError silently (user dismissed). 3. If not supported (desktop): copies the post URL to clipboard with navigator.clipboard.writeText() and shows a 'Link copied!' toast. 4. After share or copy: inserts a row to the 'shares' Supabase table with content_id, content_type 'post', shared_by (current user id if logged in, null if not), platform ('native' or 'copy'). Show the share count from shares table as a badge next to the icon — increment optimistically on click. Add a 'shares' Supabase table: id uuid, content_id uuid, content_type text, shared_by uuid nullable, platform text, referrer text, created_at timestamptz. RLS: insert allowed for all (including unauthenticated); select allowed for the post author.
```

Limitations:

- navigator.share() is blocked in the Lovable preview iframe — test on the published URL only
- OG image generation for rich preview cards requires a second prompt for an Edge Function — Lovable does not generate it in the first pass
- OG image preview in WhatsApp and iMessage requires deploying to a custom domain — the Lovable subdomain works but results vary by platform

### Flutterflow — fit 3/10, 2–4 hours

The share_plus Flutter package delivers a native share sheet with one Action. Dynamic OG images require a separate web backend since FlutterFlow cannot generate server-side meta tags.

1. Add the share_plus package to your FlutterFlow project via Settings → Pub Dependencies → add 'share_plus'
2. In the Action editor for your Share button: add a Custom Action (Dart) calling Share.share(shareText, subject: postTitle) where shareText includes the deep link URL
3. Generate the share URL in a custom Dart Action: build yourapp.com/content/{content_id} as the shareable URL, or use Firebase Dynamic Links to create a short URL
4. Track the share event by adding a Firestore write Action after the share call: insert a share document (content_id, shared_by, platform: 'native', created_at) to a 'shares' Firestore collection

Limitations:

- Dynamic OG images (the preview card in WhatsApp) require a separate web backend — FlutterFlow cannot generate server-side meta tags
- Platform detection for the shares table (Twitter vs WhatsApp vs copy) is not possible with share_plus — it only knows the user initiated a share, not which target they chose
- Deep link resolver setup (Branch.io or Firebase Dynamic Links) requires configuration outside FlutterFlow in the Firebase Console and Xcode/Android Studio

### Custom — fit 4/10, 2–3 days

Full analytics pipeline with UTM parameter injection, A/B testing OG image variants per content type, and branded short URL domain. Right choice when shares are a primary growth mechanism.

1. Implement UTM parameter injection: append ?utm_source={platform}&utm_medium=share&utm_content={content_id} to every shared URL so analytics tools attribute traffic correctly
2. A/B test OG image variants: generate two different image layouts per content type and randomly assign at share time; track conversion rates per variant in the shares table
3. Set up a branded short URL domain (share.yourapp.com) with Cloudflare Workers or a Vercel redirect to hide the /api/og?id=... URL and add click analytics
4. Build a K-factor dashboard: track share events, referral visits via UTM params, and new sign-ups attributed to each share to compute the viral coefficient

Limitations:

- 2–3 days for complete implementation including UTM attribution and viral analytics
- Branded short URL domain requires DNS configuration and a redirect service — adds infrastructure complexity

## Gotchas

- **navigator.share() silently fails in Lovable preview and V0 sandbox** — 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** — 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** — 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** — 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** — 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/content-sharing
© RapidDev — https://www.rapidevelopers.com/app-features/content-sharing
