Site Page System

This document describes how Platform renders public tenant websites. It should be updated in tandem with code changes so the implementation and mental model stay aligned.

Goal

Platform serves many public sites from one Laravel application. The incoming domain determines the current Site, and the current site determines the pages, sections, navigation, footer, legal pages, redirects, and future forms available to that public website.

The core rendering flow is:

request domain
-> resolve current Site
-> resolve requested SitePage
-> render ordered SitePageSections
-> render shared layout regions such as header and footer

Current Implementation

site_pages and site_page_sections are the primary public-site content path.

The first shipped version stored homepage and legal data in sites.homepage JSON. That bridge is still present, but it should now be treated as transitional tenant-level settings, not the long-term page content source. Current allowed uses are:

meta/script bridge for seeded legacy homepages
footer data while footer content is being normalized
temporary `main_class` values for legacy static-page recreations

New public content should be represented as SitePage records with ordered SitePageSection records.

Routable public content now lives in tenant-scoped page records:

site_pages
site_page_sections
site_navigation_items
site_redirects
lead_submissions

Seeders create SitePage records for current homepages and legal pages. 3R.Media, Platform, and The Chapel | Tyler have real home pages. Every seeded site has privacy and terms pages.

Seeders also create contact pages, header/footer navigation, common legacy redirects, and a tenant sitemap can be served at /sitemap.xml. Legal footer links are rendered from legal pages separately, so footer navigation should not duplicate privacy or terms links.

The public homepage route still falls back to the legacy placeholder view when a site does not have a home page record yet. This keeps unfinished sites visible while they are waiting for real content. Platform itself should not use that fallback; its home page is seeded as a normal SitePage.

Data Model

sites
  owns domains, brand identity, and tenant-level settings

site_pages
  belongs to one site
  represents one routable public page, including home, privacy, terms, about, donate, etc.

site_page_sections
  belongs to one site page
  stores ordered component data for that page

site_navigation_items
  belongs to one site
  can point to one site page or an external URL
  appears in a named location such as header or footer

site_redirects
  belongs to one site
  redirects old tenant paths to current paths

lead_submissions
  belongs to one site
  optionally belongs to the page where the form was submitted

Current page fields:

site_id
slug
title
status
is_home
seo_title
seo_description
seo_image
published_at

Current section fields:

site_page_id
type
data
sort_order
is_active

Current navigation item fields:

site_id
site_page_id
location
label
url
sort_order
is_active
opens_new_tab

Current redirect fields:

site_id
from_path
to_path
http_code
is_active

Current lead submission fields:

site_id
site_page_id
name
email
phone
subject
message
status
metadata
read_at

Rendering Rules

The root path / renders the current site's home page.

Named paths such as /privacy, /terms, /about, or /donate render the current site's page whose slug matches the path.

A page is public only when it belongs to the current site, is active/published, and is either already published or does not require a publish date.

Sections are rendered in sort_order order. Inactive sections are ignored.

If a site has no published home page record, / falls back to the existing placeholder view. This is only a temporary holding pattern for unfinished tenant sites.

If a slug does not resolve to a page, the controller checks site_redirects for a matching one-segment path. A tenant fallback route handles deeper legacy paths. If no redirect exists, the request returns 404.

/sitemap.xml lists the current site's publicly visible pages.

POST /contact stores a tenant-scoped LeadSubmission. Forms must only submit a site_page_id that belongs to the current site.

Section Components

Section types should map to explicit Blade components. The page renderer should never evaluate arbitrary Blade names from the database.

Examples:

splash_card
intro_hero
powered_sites
hero_image
person_feature
cta_card_grid
image_location
legal_content
contact_cta

This keeps content data flexible while keeping rendering code inspectable and testable.

Dynamic sections should still have explicit PHP sources for dynamic data. For example, powered_sites is seeded as a page section, but its site list is provided by App\Services\PoweredSiteDirectory at render time. Do not store environment labels, current hosts, or other environment-specific dynamic values in section JSON.

Additional GASS/CSeeds-inspired section types should be added explicitly as they are needed:

rich_text
icon_cards
cta_banner
contact_cta
stats_band
faq
gallery
campaign_banner
donation_cta
help_pathways

What We Are Borrowing

From gass, Platform should borrow the CMS spine:

Page
PageBlock
PageResolver
BlockRenderer
menus
redirects
sitemap
lead submissions
scheduled publishing

From cseeds, Platform should borrow nonprofit/church site patterns:

donate/help/volunteer pages
campaign sections
navbar/footer composition
public page inventory
media-rich storytelling sections
privacy/terms expectations

We should harvest these patterns, not import either old app wholesale.

Seeding Strategy

Seeders are the source of truth for initial site content across local, staging, and production.

Content that should ship identically in every environment belongs in seed data. Domain names and environment-specific values belong in config or .env, then seeders resolve them at runtime.

Seeders currently create or update:

site page records
site page section records
legal page records
footer/legal links
navigation items
redirects
contact pages

The seeder is repeatable for known seeded pages. It deletes and recreates sections for those pages so section order/data stays aligned with seed data. It does not delete unrelated future pages.

When legacy sites.homepage.sections exists, the seeder still syncs those sections into the site's index page. Platform's index page is now seeded directly from section data in the seeder instead of sites.homepage.

Current seeded records include:

3r-media/index
3r-media/privacy
3r-media/terms
3r-media/contact
platform/index
platform/contact
platform/privacy
platform/terms
tyler/index
tyler/contact
tyler/privacy
tyler/terms
emory/contact
emory/privacy
emory/terms
disciplego/contact
disciplego/privacy
disciplego/terms

Not Yet

The first version of this system is not a full CMS admin.

Avoid adding these until the public rendering model is stable:

drag/drop page builder
draft preview UI
global reusable block library
new page content stored in `sites.homepage`
theme marketplace
deep nested page trees
permission-heavy editorial workflows

Current Routes

GET /
GET /{slug}
POST /contact
GET /sitemap.xml
GET /robots.txt
fallback redirects

Next

The next useful layer is likely one of:

tenant-specific navigation design
site-specific notification email settings
admin resources for pages/sections/navigation/redirects/leads
CSeeds nonprofit section types
GASS-style richer page blocks