WebProCMS includes a built-in directory — a searchable, categorized index of businesses, members, or organizations with its own public pages, an approval workflow, CSV roster import, and (with the Memberships feature) member-managed listings and a claim-your-listing flow. It's built for the classic chamber-of-commerce "member directory" and every site shaped like one: association rosters, dealer locators, "shop local" guides, vendor lists, alumni business directories, and private club or HOA resource lists.
Who it's for
If your site needs to answer "who are our members?" or "where do I find a trusted X?", this is the feature:
- Chambers of commerce — the classic member directory. Import the roster CSV you already keep, let businesses claim and maintain their own listings, and sell premium visibility with the featured tier.
- Trade & professional associations — "find a certified installer / realtor / financial advisor near you," filtered by specialty category and city.
- Manufacturers & franchises — a dealer / installer / location finder: an admin-imported list of authorized partners with maps and a Get Directions link on every listing.
- Tourism boards & "shop local" campaigns — a categorized city guide of restaurants, shops, and attractions, with photos and paid featured placement.
- Venues & planners — a preferred-vendor list: your caterers, florists, photographers, and rental companies, grouped by service.
- Schools, churches, and coworking spaces — an alumni or member business directory where the community promotes each other's businesses, self-submitted with approval.
- Nonprofits & agencies — a community resource directory: food banks, clinics, shelters, and services organized by category so people can find help fast.
- HOAs & private clubs — a members-only list of vetted contractors, sitters, and service providers that stays behind the member login.
- B2B procurement — an approved-supplier or partner index for customers and internal teams.
What it does
- A public directory at
/directory— a card grid of published listings with live search (name, tagline, city), category filter chips, an A–Z first-letter bar, and pagination. Featured listings sort first and carry a highlight badge. - Listing detail pages at
/directory/{slug}— cover photo, logo, tagline, categories, rich description, full contact block (phone, email, website, address with a Get Directions link, social profiles), an embedded map, and schema.org LocalBusiness JSON-LD plus canonical/OpenGraph tags for SEO. Detail pages are response-cached and included in the sitemap andllms.txt. - Categories — listings belong to any number of categories (Restaurants, Retail, Professional Services…). Categories are managed in the dashboard, created automatically during CSV import, and drive the public filter chips.
- CSV roster import (Dashboard → Directory → Import) — upload the spreadsheet every chamber or association already keeps. Rows are matched by name, so re-importing the same file updates listings instead of duplicating them; a
categoriescolumn accepts pipe- or semicolon-separated names and creates unknown categories on the fly. A sample CSV is downloadable from the import page. - Public submissions (off by default) — an "Add your listing" form at
/directory/submitwith a honeypot and per-IP rate limiting. Submissions land in a pending queue for approval, or publish immediately if you enable auto-approve. You can also require a member account to submit. - Approval queue — pending submissions surface in the dashboard listing table (Pending tab with a count), on the dashboard widget card, and approve with one click.
- Featured listings — a per-listing toggle for premium placement: featured listings sort to the top of the grid and get a badge. This is the natural upsell tier for paid chamber memberships.
- Expiration dates — give a listing an expiry (e.g. when a membership lapses) and it disappears from the public directory past that date without deleting anything. Clear the date to restore it.
- Paid listings (off by default) — charge for a listing, for the featured badge, or both, with automatic expiry and renewal reminders. See the next section.
- Site search integration — published listings are indexed by the site's search (Scout / Smart Search), including the MySQL FULLTEXT path.
Find it under Dashboard → Directory (off by default; toggle under Settings → Features).
Member-managed listings (with the Memberships feature)
When Memberships is enabled, the directory becomes self-service:
- Ownership — assign a listing to a member (by email) from the listing edit page, or let it happen through claims and submissions. Member-managed listings show a badge in the dashboard table.
- Claim this listing — unclaimed listing pages show an "Is this your business? Claim this listing" link. The member signs in, optionally explains their connection, and the claim lands in a review queue at the top of Dashboard → Directory. Approving a claim transfers management to that member (and closes any competing claims); rejecting it leaves the listing untouched.
- Member self-service at
/members/listings— members edit the listings they manage (name, tagline, description, contact and address details) without dashboard access. Edits go live immediately. Images stay admin-managed. - Members-only mode — flip one switch and the whole public directory requires a member login (private club / HOA directories). Members-only directories are automatically excluded from the sitemap.
- Member dashboard card — members who manage a listing (or have a claim pending) see a "Your directory listings" card on their account dashboard.
Paid listings
Turn on Charge for listings under Dashboard → Directory → Settings and the directory becomes a product. Two things are sold, priced independently, and either can be left free:
- The listing term — pay to be in the directory at all. Set a price and a term length (default a year). A public submission is then held as Awaiting payment and only joins the directory once it's paid for.
- The featured upgrade — premium placement at the top of the grid, on its own shorter term (default a month). It has its own clock, so a free listing can carry a paid badge, and when the badge lapses the listing keeps its place.
Both ride the site-wide Stripe keys (Settings → API Keys) through an embedded checkout — there is no separate merchant account, cart or product to set up. Add the webhook signing secret shown on the settings page and point a Stripe webhook at the URL printed beside it (checkout.session.* and charge.refunded).
How the money flows
- A visitor fills in
/directory/submit, is told the price up front, and lands on a checkout page for the listing. They can pay for the listing, the featured upgrade, or both in one go. - Payment settles the money, not the moderation: if you auto-approve submissions the listing goes live immediately; if you review them it still waits in the Pending queue — it just stops saying Awaiting payment.
- Every listing gets a permanent payment link. Members see it as Renew / Upgrade on
/members/listings; you can copy it off the listing edit page to chase anyone else; renewal emails carry it automatically. - Renewing early adds to the end of the current term, never on top of today — nobody loses days for paying ahead of time.
When a term runs out. Nothing is deleted. The listing drops out of the public directory (and out of site search) and shows as Expired to its owner; paying re-publishes it instantly, exactly as it was, with no second trip through the approval queue. Reminder emails go out 7 days and 1 day before expiry, and one more when it actually lapses — each with the pay link. There is deliberately no grace period during which an unpaid listing keeps showing; the reminders are the grace.
Refunds. Refund in the Stripe dashboard and the webhook does the rest: a full refund takes back exactly the days that order bought (so refunding last year's renewal on a twice-renewed listing leaves this year's standing) and drops the featured badge if the order included it. A partial refund is recorded against the order but revokes nothing — a goodwill adjustment isn't a cancellation.
Comping a listing. Approving a listing by hand from the dashboard also settles its money question — it stops asking to be paid for. The same switch is on the listing edit page.
Payments ledger at Dashboard → Directory → Payments: every paid term and upgrade, who paid, when, and the total collected net of refunds. Read-only — refunds are issued in Stripe so there is only one place money moves.
If Stripe isn't configured yet, the settings page says so and new submissions go through free rather than piling up in an unpayable queue.
Settings
Dashboard → Directory → Settings (admins only):
| Setting | Default | What it does |
|---|---|---|
| Heading / intro | "Directory" / empty | Title and optional paragraph on the public index |
| Show map | on | Embeds a map on listing detail pages (uses the site-wide map provider; consent-gated for Google when Cookie Consent is on) |
| Public submissions | off | Opens the /directory/submit form |
| Auto-approve | off | Submissions publish immediately instead of queuing |
| Require member account to submit | off | Submission form requires a member login |
| Charge for listings | off | Turns on the paid-listing funnel below |
| Listing price / term | empty / 365 days | Price to be listed at all; empty means listings are free |
| Featured price / term | empty / 30 days | Price of the featured badge; empty means it isn't sold |
| Renewal reminders | on | Emails the listing owner 7 days and 1 day before expiry |
| Add submitters to the CRM | on | Public submissions and claims become CRM leads. Correct for a public business directory; turn it off for an internal roster (staff list, HOA directory) where entries aren't prospects |
| Members can claim listings | on | Shows the claim link on unclaimed listings |
| Members-only directory | off | The whole directory requires a member login |
Technical notes
- Feature module at
app/Features/Directory/(flag keydirectory, off by default). Tables:directory_listings,directory_categories,directory_listing_category,directory_claim_requests,directory_listing_orders. Listings reference the media library (logo_media_id,cover_media_id) and members (member_id, no FK constraint — the members table only exists once Memberships has migrated). - Two clocks, deliberately separate.
expires_atis the listing term (already enforced byscopePublished());featured_untilis the badge's own term.featuredstays the single source of truth for rendering — the sweeper just switches it off when its clock runs out — so an admin-granted badge (nofeatured_until) is never touched. - Paid listings ride their own Stripe boundary (
DirectoryStripe+directory/stripe/webhook, secretstripe.webhook_secret.directory), matching every other paid module.DirectoryCheckoutStartermints oneDirectoryListingOrder+ embedded session per plan and reuses it across refreshes;DirectoryPaymentRecorderis the single landing point for both the webhook and the return page, deduped by an atomicwhereNull(paid_at)claim. - The pay page decides its state in
mount()from the?plan=query parameter, never from a Livewire round-trip — the embedded Stripe container boots from a page-load script behindwire:ignore, so the plan chooser's options are plain links that reload the page. - Every listing state change on the paid path is a model save, never a query-builder
update()— Scout's index sync rides Eloquent events, so a bulk update would leave an expired listing searchable. directory:sweep-listings(LazyCron, hourly) enforces both clocks and sends the renewal ladder. Idempotent throughexpired_at/featured_until/renewal_reminder_stage, and capped at 100 listings per stage per pass so a bulk import with one shared expiry can't stampede the mailer.- Paying fires the
directory_listing.paidwebhook and logs a CRM timeline entry on the business owner's contact — deliberately without re-firing the contact-capture rule, so renewals don't drop members back into the new-lead drip. - Public pages are module-owned app pages (like Real Estate's search/detail), not page-editor pages — they're driven by Directory Settings, not design-library rows.
- The public index and submit form are deliberately not response-cached (per-visitor search/validation state); detail pages are cached, and every dashboard mutation clears the response cache.
- Claim approval is admin-only; approving sets
member_idon the listing and auto-rejects competing pending claims.