Frontend
Frontend
Shared across every PremiumPress product. 45 classes in inc/Frontend.
AppShell AppShell.php
Web app screens — Settings ▸ Mobile. The companion mobile app's WEB build (Expo), which Frontend\Pwa serves at <site>/app/ when the owner opens the installed app there. It no longer ships inside the theme ZIP. It is 1.2 MB packed — a fifth of the Directory ZIP — and most sites never switch it on, so the build puts it on the CDN and this service downloads it when the owner presses "Download and install". WHICH BUILD. build-theme.ps1 writes inc/Frontend/app-shell.json into the staged theme: {"build":"<build>.zip","sha256":"…","bytes":N} so a theme always installs the exact app it was built with. The app talks to this theme's own wp-json/ppt/v1/app, so the two must move together. The archive's name IS its content hash, which makes every CDN copy immutable: an older theme keeps finding its own build after a newer one is uploaded. The download is checked against the sha256 before a single file is written. WHERE IT LIVES. uploads/ppt-app/<build>/ — outside the theme folder, so a theme update does not delete it. Only the file types Pwa::serveApp() can send are unpacked (never PHP), and never outside that folder. A developer's source tree still has inc/app-shell (the Expo export target); when that folder exists it wins and nothing is downloaded — the state is "bundled". STAYING CURRENT. When a theme update names a different build, WP-Cron downloads it in the background (maybeSchedule()) — the same model as Languages\Installer. A site that was already opening on the app while the theme still bundled it gets the same background install, once, so the phones that installed it keep working. A site whose owner pressed Remove is never reinstalled behind their back. CDN layout (written by dt10-backups/tools/cdn-upload-app.js): <cdn>/app/app-shell-<build>.zip
Blog Blog.php
PPT front-end blog. Serves a proper magazine-style blog at /blog/ — a list of posts on the left with a sidebar (categories, recent posts, tags) on the right and pagination at the bottom — via a rewrite rule (the same self-contained pattern as /account and /login, so it needs no "Posts page" set in Settings). The same rendering helpers are reused by index.php for native post contexts (category/tag/date archives and single posts), so the whole blog is cohesive and on-theme (design tokens, chrome header/footer).
BuilderCompat BuilderCompat.php
Page-builder compatibility shim (Divi Builder plugin + Elementor). PPT renders builder-built pages in two situations: 1. At their own permalink — handled by index.php's singular branch, where the main query IS the page, so each builder enqueues its own assets normally. 2. Assigned to a PPT slot (home / header / footer / a virtual ppt_page route) via Admin\DesignPage — here the builder content is rendered for a post that is NOT the main-query post, so the builder's own asset enqueue (which keys off the queried object) can miss it and the layout comes out unstyled. This service covers case (2): before the body renders, it pre-loads the front-end assets for whichever builder templates the current request's slots will render. Every branch is guarded by a plugin/handle check, so the whole service is inert when neither builder is installed.
Chrome Chrome.php
PPT — site chrome variants (selectable header & footer designs). The admin picks a header and footer design under Design ▸ Header & Footer; the choice is stored in the shared design option (Branding::OPTION) and applied globally by header.php / footer.php. Each variant is plain theme markup built on Bootstrap, so it inherits the active design's brand tokens. A variant can be overridden entirely by an Elementor / page template (the same picker the Pages tab uses), so a footer can be edited in Elementor when required. Headers: a design carries (or inherits from its product line) its own header — see HeaderResolver (which header renders), HeaderDefaults (product-line defaults), HeaderVariants (the pill / search / commerce / band / overlay archetypes) and HeaderParts (the shared brand / menu / search / actions / drawer pieces). The admin choice 'design' (default) defers to that; any other variant key forces it site-wide.
ChromePreview ChromePreview.php
Live header/footer preview for the Design ▸ Header/Footer admin picker. The picker used to show a hand-drawn SVG schematic of each header/footer variant. Instead it now embeds a small iframe that points here: this route renders a standalone front-end page containing ONLY the requested chrome variant, styled with the real theme CSS and the active design's colour tokens (emitted on wp_head like every front-end page). The admin scales that iframe down into the option card, so each card shows a genuine, always-current "screenshot" of the actual header/footer — not a stylised drawing. Admins only (manage_options). Self-contained: a single ?ppt_chrome_preview request served on template_redirect, no rewrite rules.
CommentGuard CommentGuard.php
reCAPTCHA guard for the WordPress comment pipeline — protects both listing REVIEWS and ordinary COMMENTS (they're all comments) from bots. Adds the reCAPTCHA widget to every front-end comment form and verifies the token when a comment is submitted. Entirely gated on Settings → API keys: when reCAPTCHA is off or unconfigured, Recaptcha::enabled() is false and this is a no-op. Listing REPORTS are handled separately (they use wp_insert_comment, which skips this pipeline) — see Ppt\Directory\ListingActions.
Consent Consent.php
Consent popups — the two front-end gates the owner switches on in Settings ▸ Company details ▸ Consent popups: - Adult consent (consent_adult) — a blocking age gate ("Are you 18 or over?"). Confirming stores the ppt_age_ok cookie; declining takes the visitor off the site — back where they came from, or a blank page (filter ppt_consent_exit_url to set one). - GDPR consent (consent_gdpr) — a slim privacy/cookie bar pinned to the bottom with Accept / Decline, stored in the ppt_consent_gdpr cookie. The choice is broadcast as a ppt:consent DOM event so analytics/integrations can gate themselves on it. Both are off by default and render nothing at all until switched on. When both are on the age gate goes first — the privacy bar only appears once the visitor is through it. Cache-safe by design: the markup is identical for every visitor and is revealed (or not) by the inline script reading the cookie, so a full-page cache can never serve one visitor's answer to another. The head curtain paints over the page the moment the document starts parsing, so restricted content is never readable behind the gate while the footer dialog loads.
ContactGate ContactGate.php
PPT — "members only" gating for the single-listing contact channels. The admin (Design ▸ Listing Page Design ▸ "Contact details") gets an on/off switch per outbound contact channel — WhatsApp, the phone reveal and the website link. Every switch is OFF by default, which is the behaviour every existing site already has: anyone, signed in or not, can use the button. Turned ON, the channel becomes members-only. The button is NOT hidden — a visitor still sees exactly the same button, in the same place, with the same label — but for a signed-OUT visitor its target is swapped for the sign-in page (returning to the listing afterwards) and the real destination is never written into the page. That last part is the point: hiding a wa.me link behind CSS or a click handler still ships the number to anyone who opens View Source, so a locked channel's href/data attribute is replaced server-side, not decorated. Config lives in the shared design option (Branding::OPTION = ppt_design) under the contact_gate key: { whatsapp:0|1, phone:0|1, website:0|1 }. Which channels are offered depends on the active Theme Profile, because not every profile draws all three — see channels(). A profile that draws none renders no card in the Design tab at all.
CustomCode CustomCode.php
Custom CSS / JS — the owner's own code from the admin Design screen, output on the FRONT END only: CSS in the header (wp_head), JS in the footer (wp_footer). Never runs in wp-admin (those hooks are front-end only). Stored in ppt_design[custom_css] / [custom_js] (see Ppt\Design\Branding).
DemoListing DemoListing.php
Virtual demo single-listing page — renders a curated SampleListings row through the REAL single-listing template using an in-memory fake WP_Post, with ZERO rows written to the database (no posts, no attachments, no comments). In demo/preview mode a search card links here (?ppt_demo=__
DirectionsPopup DirectionsPopup.php
Directions in a pop-up — Google's route to a place, shown ON the page instead of sending the visitor off to Google. One component for every "Directions" control in the theme: the Local Food places list on the homepage (Blocks\Support\PlacesNearby) and the Directory single page's "Get directions" button (Directory/templates/parts/contact-box-directory.php). Any link carrying these attributes opens it: data-ppt-directions="<lat,lng or address>" (required — the destination) data-ppt-directions-name="<dialog> support (or with scripts off) it simply opens Google Maps in a new tab, as it did before. Build the attributes with attrs() so the escaping is right. The route itself: a site on the Google map provider WITH a key gets Google's official Maps Embed API; every other site the keyless Google embed the single-listing map already uses (Directory\ListingMap::renderGoogleEmbed). The pop-up also offers the visitor's own location (secure contexts only), and deep links to Google Maps, Apple Maps and Waze for turn-by-turn navigation on a phone. Printed once, in the footer of every front-end page; it does nothing on a page with no directions link. A starting point the visitor typed is remembered for the session.
ExpiredListings ExpiredListings.php
PPT — the "expired, but still reachable" listing state. Until now a listing that ran out of time had two possible fates, neither of them good for SEO: the listings_expiry_action setting either left it fully live (nothing — still in search, still biddable, indistinguishable from a fresh listing) or pushed it to draft / pending / trash, which makes its permalink 404 and throws away every backlink and ranking the page had earned. This adds the third option the Sold flag already models: the listing STAYS publish — its URL keeps answering 200 — but it is stamped expired, dropped from search results, closed to new proposals, and marked noindex so the page can be retired from the index gracefully instead of disappearing under a 404. The related -listings section on the page keeps passing visitors (and link equity) on to live listings. Two things can expire a listing, and both stamp the same META: 1. LIFETIME — Settings ▸ Listings ▸ "On expiry" set to "Mark expired (keep page)". Applied by the shared expiry handler (Payments\CheckoutFlow::expireListing). 2. DEADLINE — the client's own "Deadline" on a Freelancer Marketplace project (_fm\ProjectSpecs). That date was previously decorative: it printed a label and nothing keyed off it, so a project whose deadline passed months ago still sat in search accepting bids. Lives in Frontend\ rather than beside Directory\Sold on purpose: Theme::boot() skips every Ppt\Directory\* service whenever a fork owns the active profile, so a shared Service placed there would never register on the very forks that need it. Sold gets away with it by being a hookless static utility.
Features Features.php
PPT — front-end feature flags that extend the hero search boxes. The default hero search is a single keyword field; extra columns are added only when the matching feature is enabled in the admin: - Location / "Where" column → Settings → API keys → Google Maps (api_google_maps_on) - Date / "When" column → Settings → Bookings (bookings_on) Hero blocks call these so the same rule applies everywhere.
FooterLinks FooterLinks.php
Built-in footer links. When no footer menu is assigned, the theme shows a row of the standard informational pages (About, Contact, FAQ, Terms, Privacy, …), each resolved through the single link resolver (Ppt\Admin\DesignPage::pageUrl) so it honours the owner's custom links and the theme-owned virtual pages. Links that resolve to nowhere real (the bare home page) are skipped. Static helper (no hooks) — called from footer.php.
HeaderDefaults HeaderDefaults.php
Product-line header defaults — which header archetype (and options) a design gets when it does not declare its own via Designs\Contracts\HasChrome. Keyed by ThemeProfiles profile key (the design's theme()). Lives in core, NOT in a per-product design folder, so every product ZIP ships the whole map. Evidence for each choice is the niche mockups in dt10-backups/mockups (see the plan / memory). Filter ppt_header_defaults to change the map; ppt_header_base_options to change the shared option defaults.
HeaderParts HeaderParts.php
Composable header pieces shared by every header archetype (and by bespoke header blocks): brand, mobile toggle, menu, search form, actions cluster, CTA, category row, the shared off-canvas drawer and its script, plus the CSS foundation. Chrome::renderHeaderClassic/Centered keep their original markup and call these via the Chrome delegates, so their output is unchanged. The new archetypes in HeaderVariants compose the same parts with the options resolved by HeaderResolver.
HeaderResolver HeaderResolver.php
Decides WHICH header renders on this request — one memoised answer per request. Precedence (first hit wins): 1. Admin preview ?ppt_preview_header=<variant|design> (admins only, nothing saved) 2. Elementor / page template assigned in Design ▸ Header & footer 3. Site-wide override — Design ▸ Header & footer set to a specific variant (the default choice 'design' means "use the design's header" and falls through) 4. Site Generator preview — the working plan's header block (header_style) or the design the plan was loaded from (source_design) 5. The active/previewed design's own declaration (Designs\Contracts\HasChrome) 6. The SidebarShell marker (video app shell) 7. The product-line default for the design's theme (HeaderDefaults) — only when a design is active, so a bare install stays on Classic 8. Classic The result is a normalised spec: ['type' => 'variant'|'block'|'template', 'key' => …, 'options' => […], 'data' => […], 'source' => 'preview'|'template'|'site'|'studio_block'|'studio_design'|'design'|'shell'|'profile'|'fallback']
HeaderTools HeaderTools.php
PPT — header language + currency switchers. Each is shown only when enabled in Settings (Language / Currency tabs) AND there is more than one option to pick. - Language: sets a ppt_lang cookie, then filterLocale() applies it to WordPress's locale filter on the front end, so core, theme and plugin translations really do render in that language (given a matching .mo). - Currency: sets a ppt_currency cookie; prices re-render in that currency via Currencies::price() (converted through the rate table). Selections come in as ?l=xx / ?c=XXX links — NOT ?ppt_lang= / ?ppt_cur=, which are the COOKIE names only. This service stores the cookie on init and redirects to strip the query string, so testing with the wrong param is silent: no cookie, no redirect, page stays English, and nothing in the URL says why.
HeaderVariants HeaderVariants.php
The header ARCHETYPES a design can pick (Designs\Contracts\HasChrome) or inherit from its product line (HeaderDefaults): pill, search, commerce, band, overlay. Classic / Centered / the video shell stay in Chrome. Each archetype composes the shared HeaderParts and emits ONE inline, token-driven
, so only the rendered variant's rules ship and the markup+CSS travel together into the admin preview and Elementor seeds. Every rule that the theme.css repaint block also sets (background, border colour, link colours) is written at (0,2,0) or higher so it wins overbody.ppt-has-design .site-header regardless of source order.
HeroGeo HeroGeo.php
"Use my location" for any hero that asks where. Several product lines put a location box in their hero — Real Estate asks for a postcode, Dating asks for a city, Directory asks for a town — and all of them post the same near parameter, which SearchPage resolves either as coordinates or by geocoding a place name. So the affordance belongs here, once, rather than being written again in each fork's hero. IT ADDS NOTHING A VISITOR CANNOT USE. Geolocation needs a SECURE CONTEXT — https, or localhost — and on plain http every call fails silently. So the pin is only ever revealed where the browser can really answer, and a design that already draws its own location glyph gets that one wired rather than a second one bolted beside it. What goes into the field is "lat,lng", which every fork's SearchPage::nearCoords() already parses directly, so this needs no new endpoint and no AJAX. It also turns OFF browser autofill on the location box. Chrome offers its saved form history there — names, unrelated words, whatever the person last typed into a box called something similar — which drops a list of junk over the hero the moment the field is focused.
Home Home.php
Front-page controller: render the assigned/previewed design via the block renderer, or fall back to the home-demo template-library gallery. The lintel every design hangs from.
HomeDemo HomeDemo.php
Home-demo — the Template Library gallery. "Showroom D" layout (dt10-backups/mockups/home-demo-v2/option-d.html, approved 2026-09-20): a light page in the premiumpress.com palette. Headline beside the action boxes — one one-click member login PER ACCOUNT TYPE the fork offers (Account\DemoLogin::offeredTypes: buyer / freelancer, employer / candidate, browsing / independent / agency …) or a single members login, then the read-only admin demo, then the AI Site Generator pop-up; the designs as niche rows of screenshot-first cards; three "after you pick" tiles (Elementor, the admin dashboard mock, and the LIVE AI prompt form); a dark closing CTA. No video, no review strip, no filter chips — by request. Data-driven from Ppt\Designs\Registry (grouped by category), so new designs appear automatically, and profile-driven via Ppt\Content\ThemeProfiles so every line of copy adapts to the installed theme type: headline (ppt_home_headline), lead, action-box sub-lines, the admin-dashboard nouns/KPIs and the AI prompt examples. No CDN: fonts come from Ppt\Design\Fonts stacks (self-hosted/system). Previous layouts are archived at dt10-backups/HomeDemo.php.20260831-011721.bak (original), dt10-backups/HomeDemo.php.20260902-155718.bak (Cobalt) and dt10-backups/HomeDemo.php.20260920-084022.preoptiond.bak (Showroom Daylight/Obsidian).
Integrations Integrations.php
PPT — third-party integrations wired from Settings → API keys / Email. - Google Analytics (GA4): the gtag snippet is printed in the front-end <head> when a Measurement ID is set (skipped in wp-admin and for users who can edit the theme, so you don't track yourself). Filterable via ppt_enable_analytics. - Matomo: the same deal for a self-hosted (or Matomo Cloud) install — set the Matomo URL and Site ID and the tracker is printed in <head>. Independent of GA: a site may run neither, either, or both. When the GDPR privacy bar is switched on (Settings ▸ Company details) the tracker starts in Matomo's requireConsent mode and only tracks once the visitor accepts. - Email from name / address: applied to ALL mail WordPress sends via the wp_mail_from / wp_mail_from_name filters, from Settings → Email. - Google Maps API key: exposed through the ppt_google_maps_key filter so any map feature can read it. NOTE: GA loads a script from googletagmanager.com — the one deliberate external request, opt-in via the admin key (mirrors the Design brand-font exception). Matomo loads from the owner's own Matomo address, so it adds no third-party request at all unless they point it at one.
LiveEdit LiveEdit.php
LIVE TEXT EDITOR — the front-end mode. An admin browsing their REAL site turns this on from the admin bar, clicks any editable text, types, clicks away; the edit is saved live and the toolbar offers Undo. It is the v12 replacement for the v10 "Live Text Editor", and the answer to the standing complaint that changing one word means finding which block owns it and opening the Site Generator. DESIGN NOTES, in the order they matter: 1. NOTHING SHIPS WHEN THE MODE IS OFF — but SAVED TEXT STILL RENDERS. These are two different questions and conflating them is a bug I actually shipped and caught: the render paths pass their scope on every request, so an owner's saved text is applied for every visitor (it is their content, not an editing aid); Blocks\Renderer then asks isOn() separately before stamping any of the editor's own attributes. A logged-out visitor therefore sees the edited words and not one byte of the editor. Verified across 562 designs by tools/qa/livetext-nochange.php and over real HTTP by tools/qa/livetext-http.php. 2. A COOKIE, NOT JUST A URL PARAM. v10 used ?inline-editor=1 alone, so the mode ended silently the moment you clicked an internal link — and walking the real site fixing copy as you find it is the entire point. The param is the door; the cookie is the room. 3. ITS OWN NONCE AND ITS OWN GUARD. Admin\Studio's actions are registered on wp_ajax_nopriv_ too and its guard admits anonymous visitors while a site is in demo mode — correct for a public showroom, catastrophic as the credential for writing to a live site. Nothing here is shared with it. 4. NOT ACTIVE IN A PREVIEW. In the showroom (?design=) or demo-default mode the page on screen is not the site's live design, so an edit would be written against a scope the visitor never sees. isOn() refuses.
Maintenance Maintenance.php
Maintenance mode — Settings ▸ Maintenance mode (maintenance_on). While it is on, every front-end page answers with a standalone "back soon" page and an HTTP 503 + Retry-After, so search engines treat the outage as temporary and keep the site's rankings. Anyone AdminAccess::allowed() lets into wp-admin (administrators and editors) browses the real site as normal, with a red admin-bar reminder that the public cannot. What keeps working, and why: - wp-admin, admin-ajax, admin-post, REST and wp-cron never reach template_redirect. - wp-login.php (the themed sign-in) is its own entry point, so staff can sign in. - Payment gateway webhooks/IPNs are answered by the ppt-* plugins on init, well before this gate — orders placed before the switch still get paid. - robots.txt and favicon.ico are let through. SETUP HOLD: the same page is also served, automatically, from theme activation until the setup wizard finishes (ppt_installed) — so a fresh install never shows the public a half-configured site. It never touches the owner's maintenance_on setting and lifts itself the moment setup completes. Sites finished with the wizard's "No design" option (the demo network) are installed, so they are not held. The design is the owner's Aurora Standby theme (see css()). It is deliberately NOT drawn through the theme's header/footer: those carry the nav, consent popups, social proof and the site's content, which is exactly what the owner is hiding.
MemberCaps MemberCaps.php
Member capabilities — scoped to the listing workflow ONLY. Directory members register as Subscribers, who by default can't create posts or upload files. Rather than grant those capabilities to the role permanently (which would give every member site-wide posting powers and the wp-admin Posts/Media menus), this service grants them dynamically via user_has_cap, and only while the current request is part of adding/editing a listing: - the front-end Member Hub (account dashboard + /account/listing/<id>/ editor) - a single listing page (so the author sees the "Edit listing" button) - the admin-post save/create handlers (ppt_listing_save / ppt_listing_new) - the media-library AJAX the editor's photo picker uses (uploads + queries) - trashing/deleting one's own listing, and the PPT admin listing editor screen Everywhere else the member stays a plain Subscriber. Withheld even here: publish_posts (submissions still follow the moderation flow) and any *_others_posts (members can't touch other people's content — WordPress's ownership mapping still applies on top of these grants).
MembershipsPage MembershipsPage.php
Front-end membership pricing — the plan cards and the feature comparison table. Rendered in the Memberships page slot (Ppt\Frontend\Pages). Data-driven from the admin plans (Ppt\Admin\Memberships::active()), so plans added / removed / reordered in the admin appear here automatically. Styled with the active design's tokens. A card's bullet list is GENERATED from what the plan actually grants (Ppt\Account\Entitlements) and from nothing else. The admin's free-text Features box used to be appended after it; it was removed, because it let a card promise something the gates would refuse. What a plan advertises is now exactly what it enforces. The comparison table below the cards is the same data read the other way round — one row per entitlement, one column per plan — which is how buyers actually compare tiers. Arriving with ?need=<key> (from a locked control's upgrade link) highlights the row that sent them, so the table answers the question they came with.
MobileNav MobileNav.php
The mobile bottom bar ("the Dock") — a five-slot tab bar fixed to the foot of the screen on phones, with one slot rendered as a raised circle in the design's accent. This is the NEWDT rebuild of DT10's footer-mobilemenu.php. Same five slots and the same big flag; what is new is that a slot names a DESTINATION rather than a raw URL, so the link is resolved through the theme's own helpers and keeps working when a page is renamed, a fork overrides a class, or the visitor is signed out. Shared namespace on purpose: every product line boots this (a Ppt_xx* class would only boot for its own fork). Per-line differences live in MobileNavDefaults. One page type overrides the saved slots: a dating / escort profile, where the circle becomes "message this person" and the Messages tab becomes Search — applyListingMessage().
MobileNavDefaults MobileNavDefaults.php
Mobile bottom-bar defaults — which five destinations a product line ships with before the owner touches Settings > Mobile. Keyed by ThemeProfiles profile key, exactly like Frontend\HeaderDefaults, and for the same reason: a previewed design from another product line should get ITS line's bar, not the installed theme's. Lives in core so every product ZIP ships the whole map. Filter ppt_mobile_nav_defaults to change it. A row is array('key' => <destination>, 'label' => <caption>, 'icon' =>
Nav Nav.php
Primary navigation. Renders the menu assigned to the primary location when one exists, otherwise a sensible built-in default menu — so the header is never empty (matching the old DT behaviour). The default items are filterable via ppt_default_menu. A default item may carry a children array (each label + url); it then renders as a native <details> dropdown reusing the header switcher styles. The "Search" item uses this to showcase the search-page designs (filters on top / left, three columns, map view) — each child links to the search page with the public slayout switch so a visitor sees that layout without saving anything.
NavWalker NavWalker.php
Nav menu walker for the primary location. Renders a WordPress-assigned menu with the SAME markup the built-in default menu uses (see Nav::defaultMenu / Nav::renderDropdown), so an assigned menu inherits the identical header styling and native <details> dropdown behaviour instead of the plain