Knowledge > Products > Pro Website > Vanity URLs
Pro Website Vanity URLs
What Vanity URLs Do
Vanity URLs give Pro Website churches a custom subdomain on pewsearch.com. Instead of accessing their church page at pewsearch.com/churches/grace-community-church-springfield-il, the church can share grace-community.pewsearch.com -- a clean, memorable URL they can put on business cards, bulletins, and social media.
The vanity URL serves the exact same Pro Website content as the standard church detail page. It is a routing alias, not a separate page.
Slug Validation
Rules
The vanity slug must pass this validation before it is saved:
Regex: /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/
Rules:
- Minimum length: 3 characters
- Maximum length: 30 characters
- Must start with a lowercase letter or digit
- Must end with a lowercase letter or digit
- May contain hyphens in the middle
- No uppercase letters (auto-lowercased in UI)
- No spaces
- No underscores
- No special characters (! @ # $ % etc.)
- No consecutive hyphens (--) — not enforced by regex but rejected by UI validation
Valid Examples
| Slug | URL |
|---|---|
grace-community | grace-community.pewsearch.com |
first-baptist | first-baptist.pewsearch.com |
st-joseph | st-joseph.pewsearch.com |
thebridge | thebridge.pewsearch.com |
c3 | c3.pewsearch.com |
Invalid Examples
| Attempted Slug | Reason |
|---|---|
Grace-Community | Uppercase letters |
-grace | Starts with hyphen |
grace- | Ends with hyphen |
gr | Too short (min 3) |
this-is-a-very-long-slug-name-too | Too long (max 30) |
grace community | Contains space |
grace_community | Contains underscore |
Uniqueness
Vanity slugs must be unique across all premium_churches records. The admin UI performs a server-side uniqueness check before saving. If the slug is taken, the UI shows an error message suggesting alternatives.
Reserved Subdomains
The following subdomains are reserved and cannot be used as vanity slugs:
| Reserved | Reason |
|---|---|
www | Default web subdomain |
api | API routing |
admin | Admin dashboard |
app | Future application subdomain |
These are excluded in middleware.ts before any vanity slug lookup occurs.
Routing Architecture
Three Access Paths
A Pro Website page can be accessed via three different URL patterns:
| Path | Example | How It Works |
|---|---|---|
| Subdomain | grace-community.pewsearch.com | Middleware rewrites to /s/grace-community |
| Direct slug route | pewsearch.com/s/grace-community | Direct page render |
| Catch-all vanity | pewsearch.com/grace-community | Catch-all route, same render |
All three paths ultimately render the same page using the same data and template.
Middleware: Subdomain Detection
The middleware (middleware.ts) runs on every request and handles subdomain-to-path rewriting:
Middleware flow:
1. Extract hostname from request
2. Parse subdomain:
hostname = "grace-community.pewsearch.com"
subdomain = "grace-community"
3. Skip conditions (do NOT rewrite):
- IF hostname is localhost → skip (local dev)
- IF subdomain is in RESERVED_SUBDOMAINS (www, api, admin, app) → skip
- IF no subdomain (bare pewsearch.com) → skip
4. IF valid subdomain detected:
- Set header: x-is-subdomain = "true"
- Rewrite URL: /s/{subdomain}
- The layout reads x-is-subdomain to hide site chrome (PewSearch nav, footer)
5. Continue to Next.js page rendering
The x-is-subdomain Header
When a request comes through a subdomain, the middleware sets x-is-subdomain: true in the request headers. The root layout component reads this header and conditionally hides the standard PewSearch site chrome (main navigation bar, site-wide footer, directory search). This makes the Pro Website page feel like a standalone church website rather than a page within the PewSearch directory.
IF x-is-subdomain header is "true":
Hide: PewSearch main nav
Hide: PewSearch site footer
Hide: Directory search bar
Hide: "Back to directory" breadcrumb
Show: Only the UnifiedTemplate content (with its own StickyNav and Footer)
ELSE:
Show: Full PewSearch chrome wrapping the church page
Page Component: /s/[slug]
Located at pewsearch/web/src/app/(website)/s/[slug]/page.tsx, this is the primary rendering page for vanity URLs.
Page data loading:
1. Receive slug parameter from URL
2. Query premium_churches WHERE vanity_slug = slug
3. IF not found: fallback query churches WHERE slug = slug
4. IF still not found: return 404
5. Load full church data (join churches + premium_churches)
6. Compute denomination style (getStyleByDenomination)
7. Compute template labels
8. Render UnifiedTemplate with all data
The fallback to the church's standard slug means that /s/grace-community-church-springfield-il would also work even if the vanity slug is just grace-community. The vanity slug takes priority.
Catch-All Route: /[vanity]
Located at pewsearch/web/src/app/[vanity]/page.tsx, this is a catch-all route that also serves vanity URLs. It exists to handle the case where someone navigates to pewsearch.com/grace-community directly (without the /s/ prefix).
Catch-all behavior:
1. Receive vanity parameter from URL
2. Check if vanity matches any known route (directory, admin, etc.)
IF yes: let Next.js handle normally (not a vanity URL)
3. Query premium_churches WHERE vanity_slug = vanity
4. IF found: render Pro Website page (same as /s/[slug])
5. IF not found: return 404
This route has lower priority than explicit routes in the Next.js routing hierarchy. It only catches requests that do not match any other defined route.
ISR and Caching
Pro Website pages rendered via vanity URLs use Incremental Static Regeneration (ISR) with a 1-hour revalidation period:
export const revalidate = 3600; // 1 hour
This means:
- The first visit after a content change may show stale data (up to 1 hour)
- Subsequent visits within the revalidation window get the cached version
- After 1 hour, the next visit triggers a background regeneration
- The stale page is served while regeneration happens (stale-while-revalidate pattern)
Cache Implications
| Scenario | What Happens |
|---|---|
| Church updates staff in admin | Change visible within 1 hour |
| Church sets new vanity slug | New slug active within 1 hour, old slug may serve cached page briefly |
| Church changes hero video | New video visible within 1 hour |
| New Pro Website created | First visit generates the page, subsequent visits are cached |
There is no on-demand revalidation trigger from the admin dashboard. This is a known trade-off: simplicity over immediacy. Churches are told "changes may take up to an hour to appear."
SEO
Structured Data (JSON-LD)
Pro Website pages include structured data for search engines:
{
"@context": "https://schema.org",
"@type": "Church",
"name": "Grace Community Church",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main St",
"addressLocality": "Springfield",
"addressRegion": "IL",
"postalCode": "62701"
},
"telephone": "+1-555-123-4567",
"openingHours": [
"Su 09:00-10:30",
"Su 10:30-12:00",
"We 19:00-20:30"
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "23"
}
}
Breadcrumb Schema
For pages accessed within the PewSearch chrome (not subdomain), breadcrumb structured data is included:
{
"@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": "Grace Community Church"}
]
}
Meta Tags
Each Pro Website page generates dynamic meta tags:
<title>Grace Community Church | Springfield, IL</title>
<meta name="description" content="Welcome to Grace Community Church in Springfield, IL. Sunday worship at 9:00 AM and 10:30 AM. [First 160 chars of description]" />
<meta property="og:title" content="Grace Community Church" />
<meta property="og:description" content="..." />
<meta property="og:image" content="[poster image from hero video]" />
<link rel="canonical" href="https://grace-community.pewsearch.com" />
Canonical URL: When a vanity slug exists, the canonical URL points to the subdomain version. This tells search engines to index the subdomain URL as the primary version, avoiding duplicate content issues between the three access paths.
Local Development
Testing Subdomains Locally
Subdomain routing does not work on localhost because:
localhosthas no real DNS- Browser security policies treat subdomains differently on localhost
- The middleware explicitly skips subdomain detection for localhost
How to test vanity URLs locally:
Use the direct path instead of the subdomain:
# Instead of: grace-community.localhost:3000
# Use: localhost:3000/s/grace-community
This bypasses the middleware's subdomain detection but renders the same page component. The x-is-subdomain header will not be set, so PewSearch chrome will be visible, but the content is identical.
Data Model
| Column | Type | Constraints | Purpose |
|---|---|---|---|
vanity_slug | text | nullable, unique | The subdomain slug |
The vanity_slug column is:
- Nullable: Not all Pro Website churches choose to set a vanity URL
- Unique: No two churches can have the same vanity slug
- Indexed: For fast lookup in the middleware rewrite path
Edge Cases
| Scenario | Behavior |
|---|---|
| Church deletes vanity slug | Subdomain stops working within ISR revalidation window. /s/ and /[vanity] routes return 404 for old slug. |
| Church changes vanity slug | Old subdomain stops working, new one starts. Both subject to ISR cache timing. |
| Two churches want same slug | Second church gets uniqueness error. First-come, first-served. |
| Vanity slug conflicts with existing route | Catch-all has lowest priority; explicit routes (e.g., /directory, /admin) always win. Not a real conflict. |
| Church has no Pro Website plan | vanity_slug can exist in the DB but the page would render a basic listing, not the full UnifiedTemplate. In practice, vanity slugs are only set by Pro Website customers. |
See Also
- Overview -- product summary, data model
- Template System -- how the page renders once routed
- Sections -- what content appears on the vanity URL page
- Admin Features -- vanity slug editor in Settings sub-tab