A well-structured contact page is a must-have for any website. In this tutorial we’ll:
-
Create reusable React components (
ContactForm,ContactInfo,SocialLinks,MapEmbed). - Compose the page in the Next.js App Router (or Pages Router).
- Add full SEO – title, meta description, Open Graph, Twitter cards, canonical URL, and optional JSON-LD.
app/
contact/
page.tsx ← main page (App Router)
components/
ContactForm.tsx
ContactInfo.tsx
SocialLinks.tsx
MapEmbed.tsx
public/
og-contact.jpg ← 1200×630 Open Graph image
1. Install Dependencies
npm install lucide-react
# for Pages Router SEO
npm install next-seo
2. Reusable Components
components/ContactForm.tsx
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
});
const [status, setStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle');
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('sending');
// Replace with real API (Formspree, EmailJS, etc.)
setTimeout(() => {
setStatus('success');
setFormData({ name: '', email: '', subject: '', message: '' });
setTimeout(() => setStatus('idle'), 3000);
}, 1000);
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Full Name
</label>
<input
type="text"
id="name"
name="name"
required
value={formData.name}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
placeholder="John Doe"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<input
type="email"
id="email"
name="email"
required
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
placeholder="john@example.com"
/>
</div>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1">
Subject
</label>
<input
type="text"
id="subject"
name="subject"
required
value={formData.subject}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
placeholder="How can we help you?"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
Message
</label>
<textarea
id="message"
name="message"
required
rows={5}
value={formData.message}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition resize-none"
placeholder="Tell us more..."
/>
</div>
<button
type="submit"
disabled={status === 'sending'}
className={`w-full py-3 px-6 text-white font-medium rounded-lg transition ${
status === 'sending'
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{status === 'sending' ? 'Sending...' : 'Send Message'}
</button>
{status === 'success' && (
<p className="text-green-600 text-center font-medium">
Message sent successfully!
</p>
)}
{status === 'error' && (
<p className="text-red-600 text-center font-medium">
Failed to send. Try again.
</p>
)}
</form>
);
}
components/ContactInfo.tsx
import { Mail, Phone, MapPin } from 'lucide-react';
export default function ContactInfo() {
return (
<div className="space-y-6">
<h3 className="text-xl font-semibold text-gray-900">Get in Touch</h3>
<p className="text-gray-600">
We'd love to hear from you. Send us a message and we'll respond as soon as possible.
</p>
<div className="space-y-4">
<div className="flex items-start gap-3">
<MapPin className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="font-medium text-gray-900">Address</p>
<p className="text-gray-600">
123 Business St, Suite 100<br />New York, NY 10001
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Phone className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="font-medium text-gray-900">Phone</p>
<p className="text-gray-600">+1 (555) 123-4567</p>
</div>
</div>
<div className="flex items-start gap-3">
<Mail className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="font-medium text-gray-900">Email</p>
<p className="text-gray-600">hello@company.com</p>
</div>
</div>
</div>
</div>
);
}
components/SocialLinks.tsx
import { Facebook, Twitter, Linkedin, Instagram } from 'lucide-react';
export default function SocialLinks() {
const socials = [
{ Icon: Facebook, label: 'Facebook', href: '#' },
{ Icon: Twitter, label: 'Twitter', href: '#' },
{ Icon: Linkedin, label: 'LinkedIn', href: '#' },
{ Icon: Instagram, label: 'Instagram', href: '#' },
];
return (
<div className="flex gap-4">
{socials.map(({ Icon, label, href }) => (
<a
key={label}
href={href}
aria-label={label}
className="p-3 bg-gray-100 rounded-full hover:bg-blue-600 hover:text-white transition group"
>
<Icon className="w-5 h-5 text-gray-700 group-hover:text-white" />
</a>
))}
</div>
);
}
components/MapEmbed.tsx
export default function MapEmbed() {
return (
<div className="w-full h-64 md:h-80 rounded-lg overflow-hidden shadow-lg">
<iframe
src="https://www.google.com/maps/embed?pb=..."
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Company Location"
/>
</div>
);
}
Tip: Generate the embed URL from Google Maps → Share → Embed a map.
3. The Contact Page (App Router)
app/contact/page.tsx
import ContactForm from '@/components/ContactForm';
import ContactInfo from '@/components/ContactInfo';
import SocialLinks from '@/components/SocialLinks';
import MapEmbed from '@/components/MapEmbed';
// ──────── SEO METADATA ────────
export const metadata = {
title: 'Contact Us | Your Company Name',
description:
'Get in touch with us! Send a message, call, or visit our office. We respond within 24 hours.',
keywords:
'contact, support, reach out, customer service, Your Company Name',
openGraph: {
title: 'Contact Us | Your Company Name',
description:
'We’d love to hear from you. Send us a message and we’ll respond as soon as possible.',
url: 'https://www.yourwebsite.com/contact',
siteName: 'Your Company Name',
images: [
{
url: 'https://www.yourwebsite.com/og-contact.jpg',
width: 1200,
height: 630,
alt: 'Contact Us – Your Company Name',
},
],
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Contact Us | Your Company Name',
description: 'Get in touch with us anytime!',
images: ['https://www.yourwebsite.com/og-contact.jpg'],
},
robots: { index: true, follow: true },
alternates: { canonical: 'https://www.yourwebsite.com/contact' },
};
export default function ContactPage() {
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
{/* Hero */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">Contact Us</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Have a question or want to work together? Fill out the form below and we'll get back to you.
</p>
</div>
{/* Layout */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left column */}
<div className="lg:col-span-1 space-y-8">
<ContactInfo />
<div>
<h4 className="text-lg font-semibold text-gray-900 mb-4">Follow Us</h4>
<SocialLinks />
</div>
</div>
{/* Right column – Form */}
<div className="lg:col-span-2 bg-white p-8 rounded-xl shadow-md">
<ContactForm />
</div>
</div>
{/* Optional Map */}
<div className="mt-12">
<MapEmbed />
</div>
{/* ──────── OPTIONAL JSON-LD ──────── */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'ContactPage',
name: 'Contact Us - Your Company Name',
url: 'https://www.yourwebsite.com/contact',
description:
'Contact form and information for Your Company Name',
publisher: {
'@type': 'Organization',
name: 'Your Company Name',
logo: {
'@type': 'ImageObject',
url: 'https://www.yourwebsite.com/logo.png',
},
},
}),
}}
/>
</div>
</div>
);
}
4. Pages-Router Alternative (if you’re not on App Router)
// pages/contact.tsx
import Head from 'next/head';
import { NextSeo } from 'next-seo';
import ContactForm from '@/components/ContactForm';
import ContactInfo from '@/components/ContactInfo';
import SocialLinks from '@/components/SocialLinks';
import MapEmbed from '@/components/MapEmbed';
export default function ContactPage() {
const SEO = { /* same object as metadata above */ };
return (
<>
<NextSeo {...SEO} />
<Head>
<meta name="keywords" content="contact, support, ..." />
<meta name="robots" content="index, follow" />
</Head>
{/* ...same JSX as inside the App Router page... */}
</>
);
}
5. Open Graph Image
Create public/og-contact.jpg
Dimensions: 1200 × 630 px
Content: “Contact Us” + logo + subtle background
Tools: Canva, Figma, or Photoshop
That’s it! You now have a clean, component-based, fully SEO-optimized Contact Us page ready for production.
Top comments (0)