Phone Numbers - Developer Guide
Complete developer guide for implementing the multiple phone numbers feature with location-based display.
Overview
The Site Settings document supports multiple phone numbers that can be displayed in different locations (contact page, footer, or both). This guide covers frontend implementation, GraphQL queries, TypeScript types, and utility functions.
GraphQL Query
Basic Query
query GetSiteSettings {
SiteSettings(id: "siteSettings") {
contactPhones {
number
label {
en
es
ro
}
displayLocation
}
contactPhone
}
}
Complete Query with All Fields
query GetSiteSettings {
SiteSettings(id: "siteSettings") {
logo {
asset {
url
}
}
socialLinks {
facebook
twitter
linkedin
instagram
github
}
contactPhones {
number
label {
en
es
ro
}
displayLocation
}
contactPhone
contactEmail
address {
street
city
state
zipCode
country
}
businessHours {
monday
tuesday
wednesday
thursday
friday
saturday
sunday
}
}
}
TypeScript Types
Contact Phone Number Type
interface ContactPhoneNumber {
number: string
label?: {
en?: string
es?: string
ro?: string
}
displayLocation: ('contact' | 'footer')[]
}
Site Settings Type
interface SiteSettings {
contactPhones?: ContactPhoneNumber[]
contactPhone?: string // Legacy field
// ... other fields
}
Filtering by Location
Get Phones for Contact Page
// Get phones for contact page
const contactPhones = siteSettings.contactPhones?.filter(
phone => phone.displayLocation.includes('contact')
) || []
Get Phones for Footer
// Get phones for footer
const footerPhones = siteSettings.contactPhones?.filter(
phone => phone.displayLocation.includes('footer')
) || []
Utility Function
function getPhonesByLocation(
phones: ContactPhoneNumber[] | undefined,
location: 'contact' | 'footer'
): ContactPhoneNumber[] {
if (!phones) return []
return phones.filter(phone => phone.displayLocation.includes(location))
}
Backward Compatibility
Fallback to Legacy Field
// Fallback to legacy field if array is empty
const getPhonesWithFallback = (
siteSettings: SiteSettings,
location: 'contact' | 'footer'
): ContactPhoneNumber[] => {
const phones = siteSettings.contactPhones?.filter(
phone => phone.displayLocation.includes(location)
) || []
// Fallback to legacy field if no phones found
if (phones.length === 0 && siteSettings.contactPhone) {
return [{
number: siteSettings.contactPhone,
displayLocation: [location]
}]
}
return phones
}
Complete Utility Function
import type { SiteSettings, ContactPhoneNumber } from '@/types'
export function getPhonesByLocation(
phones: ContactPhoneNumber[] | undefined,
location: 'contact' | 'footer'
): ContactPhoneNumber[] {
if (!phones) return []
return phones.filter(phone => phone.displayLocation.includes(location))
}
export function getPhonesWithFallback(
siteSettings: SiteSettings,
location: 'contact' | 'footer'
): ContactPhoneNumber[] {
const phones = getPhonesByLocation(siteSettings.contactPhones, location)
if (phones.length === 0 && siteSettings.contactPhone) {
return [{
number: siteSettings.contactPhone,
displayLocation: [location]
}]
}
return phones
}
export function formatPhoneForLink(phoneNumber: string): string {
// Remove all non-digit characters except +
return phoneNumber.replace(/[^\d+]/g, '')
}
export function createPhoneLink(phoneNumber: string): string {
return `tel:${formatPhoneForLink(phoneNumber)}`
}
export function getFallbackPhone(siteSettings: SiteSettings): string | undefined {
return siteSettings.contactPhone
}
Display Examples
React Component - Contact Page
import { getPhonesWithFallback, createPhoneLink } from '@/lib/utils/phoneNumbers'
import type { SiteSettings } from '@/types'
interface ContactPageProps {
siteSettings: SiteSettings
locale: 'en' | 'es' | 'ro'
}
export function ContactPhones({ siteSettings, locale }: ContactPageProps) {
const phones = getPhonesWithFallback(siteSettings, 'contact')
if (phones.length === 0) {
return null
}
return (
<div className="space-y-2">
{phones.map((phone, index) => (
<a
key={index}
href={createPhoneLink(phone.number)}
className="flex items-center gap-2 hover:text-teal-600"
>
{phone.label?.[locale] && (
<span className="font-medium">{phone.label[locale]}:</span>
)}
<span>{phone.number}</span>
</a>
))}
</div>
)
}
React Component - Footer
import { getPhonesWithFallback, createPhoneLink } from '@/lib/utils/phoneNumbers'
import type { SiteSettings } from '@/types'
interface FooterProps {
siteSettings: SiteSettings
locale: 'en' | 'es' | 'ro'
}
export function FooterPhones({ siteSettings, locale }: FooterProps) {
const phones = getPhonesWithFallback(siteSettings, 'footer')
if (phones.length === 0) {
return null
}
return (
<div className="space-y-1">
{phones.map((phone, index) => (
<a
key={index}
href={createPhoneLink(phone.number)}
className="block hover:text-teal-600"
>
{phone.label?.[locale]
? `${phone.label[locale]}: ${phone.number}`
: phone.number
}
</a>
))}
</div>
)
}
Next.js Server Component
import { getSiteSettings } from '@/lib/sanity'
import { getPhonesWithFallback, createPhoneLink } from '@/lib/utils/phoneNumbers'
export default async function ContactPage() {
const siteSettings = await getSiteSettings()
const phones = getPhonesWithFallback(siteSettings, 'contact')
return (
<section>
<h2>Contact Us</h2>
<div className="space-y-2">
{phones.map((phone, index) => (
<a
key={index}
href={createPhoneLink(phone.number)}
className="flex items-center gap-2"
>
{phone.label?.en && (
<span className="font-medium">{phone.label.en}:</span>
)}
<span>{phone.number}</span>
</a>
))}
</div>
</section>
)
}
Phone Number Formatting
Format for Display
function formatPhoneForDisplay(phoneNumber: string): string {
// Keep original formatting for display
return phoneNumber
}
Format for Link
function formatPhoneForLink(phoneNumber: string): string {
// Remove spaces and special characters except +
return phoneNumber.replace(/[^\d+]/g, '')
}
International Format
function formatInternational(phoneNumber: string): string {
// Ensure international format
if (!phoneNumber.startsWith('+')) {
return `+1${phoneNumber.replace(/\D/g, '')}`
}
return phoneNumber
}
Localization
Get Label for Current Locale
function getPhoneLabel(
phone: ContactPhoneNumber,
locale: 'en' | 'es' | 'ro'
): string | undefined {
return phone.label?.[locale]
}
Display with Label
function formatPhoneWithLabel(
phone: ContactPhoneNumber,
locale: 'en' | 'es' | 'ro'
): string {
const label = getPhoneLabel(phone, locale)
return label ? `${label}: ${phone.number}` : phone.number
}
Testing
Unit Tests
import { describe, it, expect } from 'vitest'
import { getPhonesByLocation, getPhonesWithFallback } from '@/lib/utils/phoneNumbers'
describe('Phone Numbers Utils', () => {
const mockPhones = [
{
number: '+1 234 567 8900',
label: { en: 'Main' },
displayLocation: ['contact', 'footer']
},
{
number: '+1 234 567 8901',
label: { en: 'Sales' },
displayLocation: ['contact']
}
]
it('filters phones by location', () => {
const contactPhones = getPhonesByLocation(mockPhones, 'contact')
expect(contactPhones).toHaveLength(2)
const footerPhones = getPhonesByLocation(mockPhones, 'footer')
expect(footerPhones).toHaveLength(1)
})
it('falls back to legacy field', () => {
const siteSettings = {
contactPhone: '+1 234 567 8900'
}
const phones = getPhonesWithFallback(siteSettings, 'contact')
expect(phones).toHaveLength(1)
expect(phones[0].number).toBe('+1 234 567 8900')
})
})
Best Practices
1. Always Use Utility Functions
Use the provided utility functions instead of filtering manually:
// ✅ Good
const phones = getPhonesWithFallback(siteSettings, 'contact')
// ❌ Bad
const phones = siteSettings.contactPhones?.filter(...)
2. Handle Empty States
Always check if phones array is empty:
const phones = getPhonesWithFallback(siteSettings, 'contact')
if (phones.length === 0) {
return null // or show a message
}
3. Use Proper Link Format
Always format phone numbers for tel: links:
// ✅ Good
<a href={createPhoneLink(phone.number)}>
// ❌ Bad
<a href={`tel:${phone.number}`}>
4. Support Localization
Always use locale-aware labels:
const label = phone.label?.[locale] || phone.label?.en
5. Maintain Backward Compatibility
Always support the legacy contactPhone field:
const phones = getPhonesWithFallback(siteSettings, location)
Related Documentation
- Site Settings Schema - Complete schema reference
- Contact Information - Feature overview
- API Reference - API documentation
- Content Editor Guide - For content editors