Build a Bitly-style URL shortener with V0 featuring short link creation, click tracking with analytics (referrer, country, device), and a dashboard with Recharts visualizations. You'll create nanoid-based short codes, 302 redirect handling, and per-link analytics — all in about 30-60 minutes.
What you're building
Long URLs are ugly in emails, social posts, and printed materials. A URL shortener creates clean, trackable links that also provide analytics on who clicks them, from where, and on what device.
V0 generates the URL form, redirect handler, and analytics dashboard from prompts. Supabase stores links and click events. Recharts renders the analytics charts. The entire project is simple enough to build in under an hour.
The architecture uses a Server Action for link creation with nanoid, an API route handler for the redirect that logs click data, Server Components for the dashboard, and Recharts client components for analytics visualizations.
Final result
A URL shortener with link creation, click tracking, referrer and device analytics, and a management dashboard with charts.
Tech stack
Prerequisites
- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- No additional API keys or accounts needed
Build steps
Set up the links and clicks database schema
Open V0 and create a new project. Use the Connect panel to add Supabase. Create the links and clicks tables with a unique constraint on short_code for collision prevention.
1// Paste this prompt into V0's AI chat:2// Create a Supabase schema for a URL shortener:3// 1. links table: id (uuid PK), short_code (text UNIQUE NOT NULL), original_url (text NOT NULL), owner_id (uuid FK nullable), title (text), clicks (int DEFAULT 0), created_at (timestamptz), expires_at (timestamptz nullable)4// 2. clicks table: id (uuid PK), link_id (uuid FK), referrer (text), country (text), city (text), device (text), browser (text), ip_address (inet), clicked_at (timestamptz DEFAULT now())5// Add index on links.short_code for fast lookups.6// RLS: public can read links for redirect, authenticated users manage their own links.7// Generate the SQL migration.Pro tip: Use V0's prompt queuing — queue the schema prompt, then the URL form component, then the analytics dashboard as three separate prompts.
Expected result: Two tables created with a unique index on short_code and RLS policies for public redirect access and authenticated management.
Build the URL shortening form with nanoid generation
Create the main page with an Input for the URL and a Button to shorten it. The Server Action generates a 7-character nanoid short code and retries on collision.
1'use server'23import { createClient } from '@/lib/supabase/server'4import { nanoid } from 'nanoid'5import { revalidatePath } from 'next/cache'67export async function shortenUrl(formData: FormData) {8 const supabase = await createClient()9 const originalUrl = formData.get('url') as string10 const title = formData.get('title') as string1112 let shortCode = nanoid(7)13 let attempts = 01415 while (attempts < 5) {16 const { error } = await supabase.from('links').insert({17 short_code: shortCode,18 original_url: originalUrl,19 title: title || null,20 })2122 if (!error) {23 revalidatePath('/')24 return { shortCode }25 }2627 if (error.code === '23505') {28 shortCode = nanoid(7)29 attempts++30 } else {31 return { error: error.message }32 }33 }3435 return { error: 'Failed to generate unique code' }36}Expected result: Submitting a URL generates a 7-character short code. If a collision occurs (extremely rare with nanoid), it retries up to 5 times.
Create the redirect handler with click tracking
Build an API route that looks up the short code, logs click metadata, increments the click counter, and returns a 302 redirect to the original URL.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function GET(10 req: NextRequest,11 { params }: { params: Promise<{ code: string }> }12) {13 const { code } = await params1415 const { data: link } = await supabase16 .from('links')17 .select('id, original_url, expires_at')18 .eq('short_code', code)19 .single()2021 if (!link) {22 return NextResponse.redirect(new URL('/', req.url))23 }2425 if (link.expires_at && new Date(link.expires_at) < new Date()) {26 return NextResponse.redirect(new URL('/?expired=true', req.url))27 }2829 // Log click asynchronously30 const userAgent = req.headers.get('user-agent') ?? ''31 const referrer = req.headers.get('referer') ?? ''32 const ip = req.headers.get('x-forwarded-for')?.split(',')[0] ?? ''3334 supabase.from('clicks').insert({35 link_id: link.id,36 referrer,37 device: /mobile/i.test(userAgent) ? 'mobile' : 'desktop',38 browser: /chrome/i.test(userAgent) ? 'Chrome' : /firefox/i.test(userAgent) ? 'Firefox' : /safari/i.test(userAgent) ? 'Safari' : 'Other',39 ip_address: ip,40 }).then(() => {41 supabase.rpc('increment_clicks', { p_link_id: link.id })42 })4344 return NextResponse.redirect(link.original_url, { status: 302 })45}Expected result: Visiting /api/redirect/abc1234 logs the click and redirects to the original URL with a 302 status. Expired links redirect to the home page.
Build the analytics dashboard with Recharts
Create a per-link analytics page showing click trends over time, device breakdown, and referrer sources using Recharts BarChart and PieChart.
1// Paste this prompt into V0's AI chat:2// Build a link analytics page at app/links/[id]/page.tsx with:3// 1. Server Component that fetches the link and all its clicks from Supabase4// 2. Summary Cards: total clicks, unique visitors (distinct IP), top referrer5// 3. Recharts BarChart showing clicks per day for the last 30 days6// 4. Recharts PieChart showing device breakdown (mobile vs desktop)7// 5. Table showing recent clicks with columns: timestamp, referrer, device, browser, country8// 6. Badge for active/expired status9// 7. Copy button to copy the short URL10// Use shadcn/ui Card for chart containers and Table for click log.Expected result: An analytics page with summary Cards, a BarChart of clicks over time, PieChart of device breakdown, and a Table of recent individual clicks.
Build the link management dashboard
Create a dashboard page showing all the user's shortened links in a Table with click counts, creation dates, and action buttons for copying, viewing analytics, and deleting.
1// Paste this prompt into V0's AI chat:2// Build a link management dashboard at app/dashboard/page.tsx with:3// 1. Server Component fetching all links for the current user4// 2. shadcn/ui Table with columns: Short URL (clickable), Original URL (truncated), Clicks, Created, Status Badge (active/expired)5// 3. Each row has a copy button (copies short URL to clipboard) and a link to analytics6// 4. "Shorten URL" Button at the top that opens a Dialog with Input for URL and optional title7// 5. Delete Button with AlertDialog confirmation8// 6. Summary Cards at top: total links, total clicks, clicks today9// Use Badge variant 'default' for active links and 'destructive' for expired.Expected result: A dashboard Table showing all shortened links with click counts, status Badges, copy buttons, analytics links, and summary Cards at the top.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function GET(10 req: NextRequest,11 { params }: { params: Promise<{ code: string }> }12) {13 const { code } = await params1415 const { data: link } = await supabase16 .from('links')17 .select('id, original_url, expires_at')18 .eq('short_code', code)19 .single()2021 if (!link) {22 return NextResponse.redirect(new URL('/', req.url))23 }2425 if (link.expires_at && new Date(link.expires_at) < new Date()) {26 return NextResponse.redirect(new URL('/?expired=true', req.url))27 }2829 const userAgent = req.headers.get('user-agent') ?? ''30 const referrer = req.headers.get('referer') ?? ''3132 supabase33 .from('clicks')34 .insert({35 link_id: link.id,36 referrer,37 device: /mobile/i.test(userAgent) ? 'mobile' : 'desktop',38 browser: /chrome/i.test(userAgent)39 ? 'Chrome'40 : /safari/i.test(userAgent)41 ? 'Safari'42 : 'Other',43 })44 .then(() =>45 supabase.rpc('increment_clicks', { p_link_id: link.id })46 )4748 return NextResponse.redirect(link.original_url, { status: 302 })49}Customization ideas
Add custom short codes
Let users choose their own short code (e.g., /my-brand) instead of random nanoid. Validate uniqueness and length restrictions.
Add QR code generation
Generate a QR code for each short link using a library like qrcode.react. Display it on the analytics page with a download button.
Add link expiration settings
Let users set expiration dates on links. Show a countdown on the dashboard and redirect expired links to a custom 'link expired' page.
Add UTM parameter builder
Add a form that helps users append UTM parameters (source, medium, campaign) to their original URL before shortening for marketing tracking.
Common pitfalls
Pitfall: Using sequential IDs or short random strings for short codes
How to avoid: Use nanoid(7) which generates URL-safe random strings with 2B+ combinations. Retry on unique constraint violation to handle the rare collision.
Pitfall: Logging clicks synchronously before redirecting
How to avoid: Fire the click insert as a fire-and-forget promise. The redirect returns immediately while the click logs asynchronously.
Pitfall: Not handling expired links
How to avoid: Check expires_at in the redirect handler and redirect expired links to a friendly 'link expired' page instead of the original URL.
Best practices
- Use nanoid(7) for short code generation — URL-safe, collision-resistant, and compact enough for short URLs
- Log clicks asynchronously (fire-and-forget) so the redirect response is instant without database latency
- Use V0's prompt queuing to build the URL form, redirect handler, and analytics dashboard as three queued prompts
- Create an atomic increment_clicks RPC function in Supabase to prevent count drift from concurrent clicks
- Use Design Mode (Option+D) to visually polish the URL form and analytics charts at zero credit cost
- Set up a custom domain in Vercel project settings for branded short links (e.g., yourbrand.link/abc1234)
- Store the short link domain as an environment variable so it works correctly in both development and production
AI prompts to try
Copy these prompts to build this project faster.
I'm building a URL shortener with Next.js App Router and Supabase. I need: 1) Short code generation with nanoid and collision retry, 2) A redirect handler that logs clicks asynchronously, 3) Click analytics with referrer, device, and time data, 4) Recharts charts for the analytics dashboard. Help me design the schema and the async click logging pattern.
Create a URL redirect handler at app/api/redirect/[code]/route.ts that: 1) Looks up the short_code in Supabase links table, 2) Returns 302 redirect to original_url, 3) Logs click metadata (referrer, user-agent parsed device/browser) asynchronously without blocking the redirect, 4) Handles missing links with redirect to home page, 5) Handles expired links. Use fire-and-forget Supabase insert for click logging.
Frequently asked questions
How are short codes generated?
Short codes are generated using nanoid(7), which creates 7-character URL-safe random strings. With 2 billion possible combinations, collisions are extremely rare. If a collision occurs, the Server Action retries with a new nanoid up to 5 times.
Can I use a custom domain for short links?
Yes. Add a custom domain in your Vercel project settings (e.g., yourbrand.link). Then set NEXT_PUBLIC_SHORT_DOMAIN in V0's Vars tab so the UI displays the correct short URL.
What V0 plan do I need?
V0 Free tier works perfectly. The URL shortener is a simple project with basic Server Components, one API route, and shadcn/ui components.
How accurate are the analytics?
Click tracking captures referrer, user agent (parsed into device and browser), and IP address. Country and city detection requires a third-party IP geolocation API which can be added as an enhancement.
How do I deploy this?
Click Share > Publish in V0. The Supabase connection is auto-configured. Short links start working immediately with the Vercel production URL.
Can RapidDev help build a custom link management platform?
Yes. RapidDev has built 600+ apps including marketing platforms with link tracking, UTM management, and conversion analytics. Book a free consultation to discuss your link management needs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation