Skip to main content

Documentation

No results found.
Features

Locations

WebProCMS ships with a built-in Locations content type that manages a roster of business addresses — store fronts, franchise outlets, dealer locations, regional offices — from one place in the dashboard. Each location has its own public det...

WebProCMS ships with a built-in Locations content type that manages a roster of business addresses — store fronts, franchise outlets, dealer locations, regional offices — from one place in the dashboard. Each location has its own public detail page, and design library rows can pull a live, filtered list of locations as a data source so a "Find a location" page or a city-filtered list updates automatically when the underlying records change.


The problem

A site that represents more than one physical place — a franchise, a multi-office firm, a chain of clinics, a dealer network — needs a list of locations on the public site, a detail page per location, sometimes a state filter, and almost always editor access to update an address or a phone number without rebuilding the page. Hand-rolling that as plain content drifts immediately: the same address gets pasted into a footer, a contact page, and three city landing pages, and now the editor has to find and update every copy when a location moves.

The fix

Locations is a seeded content type — the same dynamic-field, relations, and design-library system every other content type in the CMS (blog, events, services, portfolio, …) is built on. Every location has a stable record (name, address, phone, hours, and more), the public site renders a listing page and a per-location detail page, and the data source preset lets any design library row pull the live list with filtering and ordering. Editing an address in one place updates everywhere it's referenced.

Locations seeds unconditionally like blog and services — there's no feature toggle to turn it on, and it isn't gated behind a membership feature the way Events is. It's a content type, not a bespoke module: its schema, dashboard CRUD, and public pages are the same generic content-type machinery every type shares, defined in LocationContentTypeBlueprint so the seeder, the scaffold command, and test fixtures all stay in sync.

What you can store per location

Field Type Notes
name text Display name.
detail_page_heading text Optional H1 override for the detail page.
address address The international address field — line 1/2, city, region, postal code, country. Drives the region/state facet and the directions link. Also holds the optional manual map pin (see below).
phone text
sms text
fax text
hours hours Structured per-day open/close times, rendered via OpeningHours and mapped into LocalBusiness JSON-LD openingHours.
schedule_url / schedule_label / schedule_new_tab text / text / toggle An optional "Schedule" or "Book" link with its own button label and new-tab behaviour.
reviews_eyebrow / reviews_image text / image Copy shown above the location's reviews block.
services, team, reviews, faqs relation "Show all of type" relations — each defaults to every published record of the related content type (services, team members, reviews, FAQs), so a location automatically lists your whole services menu unless you scope it.

There's no Google Places ID field — the address field plus a photo (via the standard featured-image picker) cover the franchise / multi-office / dealer-network cases that drove the design. Because Locations is an ordinary content type, an admin can add, remove, or reorder fields from Dashboard → Content Types → Locations at any time — the table above is the seeded default, not a fixed schema.

When the map can't find an address

Maps normally place their pin by looking the written address up. Some real addresses have no entry in the map data — a highway frontage road, a suite in a plaza, a town the map service files under its larger neighbour — and those locations come out pinless while the rest of the map is fine.

Every address field has an optional manual pin for exactly that: click Set the map pin manually under the address and enter a latitude and longitude (right-click a spot in any mapping app to copy them). The address keeps displaying exactly as written, so you never have to reword a real address to satisfy the map. Coordinates apply everywhere that address is mapped — the locations list, the location's own page, a contact map — with no page rebuilding, and they take priority over anything the lookup would return. Clear both fields to hand the pin back to the automatic lookup. This is available on any content type with an address field, not just Locations.

Status

Like every content type, a location is draft, published, or scheduled (publishes automatically once its published_at time arrives). The published scope — status = 'published' and published_at <= now() — is what the public listing page and the design library data source preset use.

Where it lives in the dashboard

  • Dashboard → Content Types → Locations edits the schema — add/remove/reorder fields, set the default list/detail layouts, the LocalBusiness JSON-LD mapping.
  • Individual locations are managed at Dashboard → Content → Locations (dashboard/content/locations) — the same generic content-item list/create/edit screens every content type uses, with a photo picker, status select, and a Save dropdown exposing Save + Exit / View / Add New / Next.
  • The Locations type sets show_dashboard_button, so it also gets a quick-access card on the main admin dashboard linking straight into its item list.

The photo field uses the standard media library picker contract — the picker dispatches media-image-picked and the location form stores the resulting media_items.id on the item's featured image. Replacing or deleting the underlying media item flows through the same usage-warning + cache-busting path every other entity uses.

Public pages

Two routes ship by default, generated by ContentTypePageGenerator exactly like every other content type's pages (blog, services, events, …) — there's no bespoke locations controller:

Route Template
locations.index A listing page that the user controls via the design library.
locations.show The per-location detail page, addressed by the location's slug.

Both routes are ordinary Livewire full-page components (pages::locations.index / pages::locations.show) backed by App\Models\ContentItem with type_slug = 'locations'. The detail route is what the data source preset's url token returns, so a "View details" link inside a row on the listing page resolves to the right per-location URL automatically.

ContentItem clears the response cache on every save and delete, so a location edit propagates to the public site without any manual cache flush.

Pulling locations into design library rows

Locations is bound into the design library through the same generic ContentTypePreset every content type uses — registered as data source key content_type:locations. Any row that supports a data source field — directory grids, repeating cards, list rows — can pick "Locations" and bind to the live list.

The preset derives locations-specific behaviour generically from the field schema: the address field yields a region/state filter (auto-populated from the distinct list of values across published locations) plus {item.directions_url} / {item.schedule_booking_url} tokens, the hours field formats via OpeningHours::formatLines(), and the relation fields (services, team, reviews, faqs) resolve through rel_{name}.* tokens. There's no separate "available states" list to maintain — it's built from the actual published records.

Scope is always ContentItem::query()->where('type_slug', 'locations')->published() — drafts and scheduled-but-not-yet-published records never appear.

Detail page layouts

Locations ships a bespoke Location Detail design (@requiresContentType locations) alongside the shared, field-agnostic detail layouts (Article, With Sidebar, Image Sidebar, and more) every content type can pick from — plus map-focused variants (Hero, Full-bleed Card, Stat Strip, Visit Panel) and supporting blocks (Location Team, Location Services, Location Reviews, Location FAQs) used by the type's detail_sections. See per-record detail layouts for how the picker works.

What lives where

Path Purpose
app/Support/LocationContentTypeBlueprint.php The canonical field schema + type attributes, shared by the seeder, the scaffold command, and test fixtures.
app/Models/ContentItem.php The Eloquent model backing every location record (type_slug = 'locations').
app/Support/DataSources/Presets/ContentTypePreset.php The generic data source preset every content type — including locations — registers under content_type:{slug}.
app/Support/ContentTypePageGenerator.php Generates resources/views/pages/locations/⚡{index,show}.blade.php and injects the locations.index / locations.show routes.
app/Support/OpeningHours.php The hours field's value object — formatting + JSON-LD openingHours mapping.
app/Support/AddressFormats.php The international address field's country/region formatting rules.
resources/design-library/rows/page-layouts/detail/location-*.blade.php The bespoke Location Detail layout + map variants + team/services/reviews/FAQs blocks.

Marketing angle

If you run a franchise, a multi-office firm, a clinic chain, or a dealer network, Locations is the part of WebProCMS you'll touch most. Add a location once and it shows up everywhere — the public locations page, the region filter, the city list on the homepage, the footer "find us" block — without copy-pasting an address. Move a store, swap a phone number, or take a location offline temporarily, and every page that pulls from the locations data source updates the moment you save.

Notes

  • Locations is not a feature module — it lives in the content-type system (ContentItem + LocationContentTypeBlueprint) rather than under app/Features/. It seeds unconditionally and isn't behind a membership feature flag, so every install has it.
  • Because it's a content type, it inherits everything content types get for free: the media library, Scout search, scheduled publishing, previous_slugs redirects, translations, the SEO meta editor, the per-record detail-layout picker, relations to other content types, and comments (if enabled on the type).
  • The is_seeded flag on demo location items is used by the demo data seeder so demo locations can be cleaned up in bulk without affecting real ones.