Skip to main content

Knowledge > Products > PewSearch Directory > Church Detail Page

Church Detail Page

What It Is

The church detail page is the single most important page in PewSearch. It is the canonical URL for every church in the directory -- the page that ranks in Google, the page visitors land on when searching for a specific church, and the page where the claim/upgrade funnel begins.

Route: /churches/[slug] File: pewsearch/web/src/app/churches/[slug]/page.tsx

Each of the 218K+ visible churches has a detail page. These pages are statically generated at build time for high-traffic churches and rendered on-demand for the long tail.

Data Displayed

The detail page renders everything known about a church, organized into visual sections:

Header Section

ElementSourceNotes
Church namechurch.nameH1 tag, primary heading
Denomination badgechurch.denominationColored badge, links to denomination page
Premium badgechurch.is_premiumGold verified badge if claimed
Rating starschurch.rating1-5 stars from Google Maps
Review countchurch.reviews_count"(X reviews)" label
Photochurch.photo_urlHero image, falls back to placeholder
Logochurch.logo_urlCircular overlay on hero (if available)

Contact & Location Section

ElementSourceNotes
Full addresschurch.addressWith copy-to-clipboard
Phone numberchurch.phoneClick-to-call link
Websitechurch.websiteExternal link (opens new tab)
Google Maps linkchurch.google_maps_url"Get Directions" button
Interactive mapchurch.latitude, church.longitudeLeaflet map with pin

Service Times Section

ElementSourceNotes
Working hourschurch.working_hoursJSONB: {"Sunday": ["9:00 AM", "11:00 AM"], "Wednesday": ["7:00 PM"]}
Custom hours (Premium)premium.custom_hoursOverrides working_hours when available
Next service highlightComputed from hours"Next service: Sunday at 9:00 AM" callout

The hours display logic:

pseudocode: resolveHours(church, premium)
if premium exists AND premium.custom_hours is not empty:
return premium.custom_hours // Pastor-provided hours take priority
else if church.working_hours is not empty:
return church.working_hours // Google Maps scraped hours
else:
return null // Show "Hours not available" message

About Section

The church.about JSONB field contains up to ~20 metadata fields scraped from Google Maps:

FieldTypeExample
accessibilityJSONB{"Wheelchair accessible entrance": true, "Wheelchair accessible parking": true}
amenitiesJSONB{"Restroom": true}
atmosphereJSONB{"LGBTQ+ friendly": true}
crowdJSONB{"Groups": true}
from_the_businessJSONB{"Identifies as women-led": true}
highlightsJSONB{"Great for kids": true}
offeringsJSONB{"Groups": true, "Youth programs": true}
paymentsJSONB{"Debit cards": true}
planningJSONB{"Appointment required": false}
popular_forJSONB{"Worship services": true}
service_optionsJSONB{"Online services": true, "Onsite services": true}

Each non-empty field renders as a collapsible section with icon badges.

Photos Grid

ElementSourceNotes
Primary photochurch.photo_urlLarge hero display
Logochurch.logo_urlCircular display
Additional photosPremium uploadsOnly for Premium subscribers

Nearby Churches

A "Nearby Churches" section shows churches within a configurable radius:

pseudocode: getNearbyChurches(lat, lng, radiusMiles=10, limit=6)
SELECT id, name, slug, address, city, state_code, photo_url,
denomination, rating, reviews_count,
(haversine_distance(lat, lng, church.latitude, church.longitude)) as distance
FROM churches
WHERE directory_visible = true
AND business_status = 'OPERATIONAL'
AND distance <= radiusMiles
AND id != current_church_id
ORDER BY distance ASC
LIMIT limit

This uses PostGIS-compatible distance calculation. The query runs server-side and returns the nearest 6 churches.

Claim CTA

Every non-premium church shows a sticky "Claim This Church" call-to-action:

StateCTA TextLink
Unclaimed"Is this your church? Claim it for free."/claim/[slug]
Claimed (free)"Upgrade to Premium"/pricing
PremiumNo CTA (already subscribed)--

The CTA is sticky at the bottom of the page on mobile and appears in the sidebar on desktop. It is the primary conversion point for the PewSearch funnel.

Pastor Lead Capture

Below the hours section, a contextual CTA targets pastors:

"Are you the pastor of [Church Name]?"
"Claim your free listing to update hours, add photos, and connect with visitors."
[Claim This Church →]

This is distinct from the sticky CTA and appears inline in the content flow.

Vanity URL Support

Premium churches can set a vanity_slug in premium_churches. When a vanity slug is configured, the church is accessible at both:

  • /churches/[original-slug] (canonical)
  • /churches/[vanity-slug] (redirect to canonical)

The vanity slug lookup happens in the page's data fetching:

pseudocode: resolveChurchBySlug(slug)
// First try direct match
church = query churches WHERE slug = slug
if church exists:
return church

// Then try vanity slug
premium = query premium_churches WHERE vanity_slug = slug
if premium exists:
church = query churches WHERE id = premium.church_id
redirect to /churches/[church.slug] // 301 redirect

// Not found
return 404

SEO Implementation

JSON-LD Structured Data

Every church detail page emits JSON-LD structured data for Google:

{
"@context": "https://schema.org",
"@type": "Church",
"name": "Grace Community Church",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main St",
"addressLocality": "Dallas",
"addressRegion": "TX",
"postalCode": "75201"
},
"telephone": "+1-214-555-0100",
"url": "https://www.gracecommunity.org",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "Sunday",
"opens": "09:00",
"closes": "12:00"
}
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"reviewCount": "142"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 32.7767,
"longitude": -96.7970
}
}
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Home", "item": "https://pewsearch.com"},
{"@type": "ListItem", "position": 2, "name": "Directory", "item": "https://pewsearch.com/directory"},
{"@type": "ListItem", "position": 3, "name": "Texas", "item": "https://pewsearch.com/directory?state=TX"},
{"@type": "ListItem", "position": 4, "name": "Grace Community Church"}
]
}

Meta Tags

TagTemplate
<title>`{name} - {city}, {state}
meta description{name} in {city}, {state_code}. {denomination}. Service times, directions, contact info, and more.
og:titleSame as <title>
og:descriptionSame as meta description
og:imagechurch.photo_url or PewSearch default OG image
canonicalhttps://pewsearch.com/churches/{slug}

Church Type Definition

The full type returned by getChurchBySlug() in queries.ts:

interface Church {
id: string;
name: string;
slug: string;
address: string;
street: string | null;
city: string;
state: string;
state_code: string;
zip_code: string | null;
latitude: number | null;
longitude: number | null;
phone: string | null;
website: string | null;
category: string | null;
subtypes: string[] | null;
denomination: string | null;
lgbtq_inclusive: boolean | null;
inclusivity_signals: string[] | null;
rating: number | null;
reviews_count: number | null;
photos_count: number | null;
photo_url: string | null;
logo_url: string | null;
description: string | null;
about: Record<string, unknown> | null; // ~20 JSONB fields
working_hours: Record<string, string[]> | null;
google_maps_url: string | null;
is_premium: boolean;
created_at: string;
website_scraped_at: string | null;
}

Performance Considerations

  • Static generation: High-traffic church pages are ISR (Incremental Static Regeneration) with revalidation
  • Image optimization: photo_url and logo_url are served through Next.js Image component with lazy loading
  • Map lazy loading: The Leaflet map only loads when the user scrolls to the map section (intersection observer)
  • Nearby churches: Server-side query, not client-side -- avoids sending all church coordinates to the browser

See Also