Directory Theme class reference
Real Estate Theme — Class reference
The 38 classes that make _rt behave differently from the shared Directory core. Generated from the theme source.
← Back to Real Estate Theme features
AgencyHub AgencyHub.php
The demo agent's agency page. The one-click "Agent login" (Account\DemoLogin) should land on a My Agency panel that is already filled in — name, area, an introduction, a website and a logo, the way the escort demo agency and the coupon demo store arrive — not on an empty "create" form. The roster is the lent properties (AgencyTax::create backfills them by author). The page is removed again when the demo member is purged. Mirror of Ppt_es\AgencyHub minus the hub-view swap: the Real Estate hub is the shared Atlas one already.
AgencyTax AgencyTax.php
Agencies — the estate and letting agencies advertising on this site, as a real taxonomy. A clone of Ppt_es\AgencyTax for the Real Estate fork (2026-09-20), because the two products have the same shape: Account\AccountType asks at sign-up whether the member is an agent (ppt_account_type = agency), the Member Hub gives an agent a roster of properties, and what the agent lacked was a PLACE — a public page for the agency itself, exactly the gap the escort fork filled and the coupon fork's stores fill. So the agency becomes a listing_agency term, and the term is where the public identity lives — logo, blurb, area and website, edited by the agent from the Member Hub ("My Agency"). Two surfaces come out of it: /agencies/ the index of every agency (Frontend\Pages' agencies slot) /agencies/<slug>/ one agency: its header + the properties it lists (the ordinary listing archive, with a header injected at ppt_category_before_results) THE ACCOUNT OWNS THE TERM: META_OWNER holds the user id, USER_META the term id, both written together by {@see create()} — one agency per account, and only an account that answered "I am an agent" at sign-up may have one ({@see canCreate()}). THE ROSTER IS DERIVED, NEVER TYPED: a property authored by an agency account is filed under that agency's term on save ({@see onSave}), so the hub roster and the public archive are two views of one fact (post_author) and cannot disagree. Kept as a per-fork copy rather than a shared class on purpose: inc/_es ships only in the Escort build and inc/_rt only in the Real Estate build, and the two products' wording (escorts / properties) comes from AccountType::rosterLabels(), which is already fork-aware. Fix a bug in one → fix it in the other.
DemoAccounts DemoAccounts.php
Real Estate — what the typed DEMO members walk into on top of the shared furnishing (Account\DemoFurnish: favourites, follow, inbox thread, review). the OWNER (AccountType::SOLO) two seeded properties (DemoLogin lends them — the count is raised here), switched to the built-in viewing provider, with the seeker's viewing requests on them: one confirmed, one pending, one already completed — and an enquiry message about each property in the inbox; the AGENT (AccountType::AGENCY) four properties, the same treatment; the SEEKER (AccountType::CLIENT) those viewings in "My bookings", and the enquiries it sent. Viewings go through the shared reservation engine (Bookings::create / setStatus), so both hubs read exactly what a real request leaves behind; enquiries are Messages, which is what the "Send enquiry" button on a property sends. Removed on the pair's unfurnish action. Gated to the realestate profile by Theme::boot()'s fork-gating (Ppt_rt*).
DemoProfiles DemoProfiles.php
Real Estate Theme (_rt) — demo property data. Tools\SampleData resolves Ppt\\DemoProfiles per fork and calls whatever it declares: apply() for the fork's own meta, and body/faq/specifications for copy the shared filler gets wrong. Declaring a method IS the opt-in, so nothing in the seeder needs to know this class exists. WHY THIS EXISTS A seeded property used to carry no property data at all. The single page filled the gap from ListingCard::sidebarRows()'s $preview placeholders, so every listing on every Real Estate design showed the same invented "3 bed / 2 bath / 3,410 sq ft / built 2016 / HZ26" — and once a client went live with real listings, the panel had nothing to show. This writes REAL meta instead, so the details are true on the demo site and the placeholders are never reached. WHERE THE NUMBERS COME FROM Not invented per row in a second table that could drift out of step with the copy: they are READ OFF the curated row itself. "Six Bed House, Hyde Park Terrace" gets six bedrooms; "Seven bedrooms, a coach house and just under an acre" gets seven; "Built in 2016 with underfloor heating" gets 2016; "a double garage" gets two. Only what the row leaves unsaid falls back to a per-category default, and every fallback is deterministic in the row index so a re-seed rebuilds the same site. NOT EVERY LISTING IS A HOUSE A plot of land has no bedrooms and an office has no bathrooms. Land and commercial rows are shaped differently on purpose (see shapes()), and a card renders only the rows that have a value — so a building plot shows its status, type and reference and nothing it could not truthfully have.
Expiry Expiry.php
PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.
Gallery Gallery.php
Single-listing gallery styles. The admin picks one of four layouts on the Design ▸ Listings tab (stored in the ppt_design option via Branding), and the single-listing template renders the chosen layout for the listing's images. standard — large hero photo + thumbnail strip (click to swap) [default] grid — all photos in a tiled grid (first one featured) carousel — a swipeable/scroll-snap slider with prev/next tall — full-width photos stacked vertically (good for portraits)
Geocoder Geocoder.php
Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.
HeroFilters HeroFilters.php
Turning a designer's words into a real search filter. A hero's fields are editable text — "Shared houses", "Bills included", "Freehold" — because that is what makes a design a design. The search needs term IDs. This is the one place that joins the two, so the three heroes that do it cannot drift apart into three slightly different alias tables. Everything here fails SOFT: a label that matches nothing returns 0, and the caller is expected to render that control as plain text rather than as a filter which would send the visitor to an empty results page. A half-matched hero degrades to a keyword box, never to a broken one.
ListingActions ListingActions.php
Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.
ListingCard ListingCard.php
The canonical listing card — the single, theme-wide way a business/listing is shown as a card. Every directory surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a listing looks the same everywhere and the demo-vs-live data split lives in ONE place. Data-source agnostic: fromPost() maps a real listing_type record and fromSample() maps a curated demo row into the same normalized shape, which render() draws. The markup is built on the theme design tokens, so the one card automatically adopts each design's colours/fonts. Its CSS is enqueued site-wide (assets/css/listing-card.css) — the card only emits markup. Normalized shape: name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id badge is an optional ribbon label (e.g. "New") for callers that need one; when empty it falls back to "Featured" if featured is set.
ListingEditor ListingEditor.php
Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.
ListingFaq ListingFaq.php
Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.
ListingHours ListingHours.php
Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.
ListingLocation ListingLocation.php
Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().
ListingMap ListingMap.php
The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.
ListingPricing ListingPricing.php
Single-listing pricing plans. A listing owner enters one or more plans / packages on the submission form (a name, a price, and a short description); they're stored in the pricing_plans post meta as a list of ['name' => …, 'price' => float|null, 'desc' => …]. The LOWEST priced plan is mirrored into the searchable price meta (Ppt_rt\SearchPage::PRICE_META) by the editor on save, so the price range filter, the price sort, and the result-card price all work — that's the link between "enter your pricing" and "find listings by price". Prices are stored in the site's base currency; render() converts + formats via Ppt\Content\Currencies for the viewer's chosen currency.
ListingSections ListingSections.php
PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.
ListingTiles ListingTiles.php
The tile row at the top of the Real Estate listing form: Category, Property type, Amenities and Status. Category used to be a stacked chip-and-button field under the title — a row of chips, a "Choose category" button and a line of hint text, three stacked things for one decision. It now reads as the same square tile the Directory theme uses (Ppt\Directory\ListingTiles) and the Coupon and Software themes before it: icon, label, and either the chips that are set or the word "Select". The tile opens the SHARED category picker modal (assets/js/category-picker.js) — searchable, with a Clear All / Continue footer — so there is still exactly one picker implementation across every fork. Property type, Amenities and Status are the three Real-Estate vocabularies (Ppt_rt\PropertyDwelling, PropertyAmenities, PropertyStatus) — the free-text type box, the card of 23 amenity checkboxes and the status <select> they replace were three more controls, in three different places on the form, for what is really the same kind of decision. All four tiles run the same shared picker; Property type and Status are single-pick (data-cat-multi="0"). No Business hours tile here: Real Estate has no opening hours at all (ListingSections::unsupported()). The grid is auto-FILL, so the row keeps tile-sized tracks whatever the count.
ListingViews ListingViews.php
PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).
Media Media.php
Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.
MediaTabs MediaTabs.php
Real Estate Theme — the media switcher over the listing gallery. A property is more than one thing to look at: the photographs, where it is on a map, and the video if the agent shot one. Every portal puts those on one control over the hero image, because a buyer flips between them constantly. EVERY TAB IS EARNED. A tab is only drawn when there is really something behind it: Photos the gallery has at least one image Map Maps is on for the site AND the listing has coordinates or an address Video the listing has at least one video THERE IS NO STREET VIEW, deliberately. It is the obvious fourth tab and it was built, then taken out again: the theme ships keyless on OpenStreetMap tiles, and the only realistic source of street-level imagery is Google's Street View Embed API, which wants a billed key. Most owners will never set one up, so the tab would be absent on most sites while still costing every site the code that checks — and a feature that is really "only if you pay Google" is better not advertised at all. If that changes, the shape to add it back is one entry in available() and one in paneHtml(); the switcher itself is data-driven and does not care how many tabs it is given. With fewer than two tabs earned the whole bar goes — a switcher offering one choice is furniture, not a control.
PropertyAmenities PropertyAmenities.php
Amenities — the "Key features & amenities" list as a real TAXONOMY. Amenities used to be a fixed 23-item checklist stored as an array of keys in post meta (ppt_rt_amenities). A fixed list cannot be extended by the site owner, has no page of its own, and can't be filtered on: "show me everything with a pool" had nowhere to look. So the list becomes listing_rt_amenity terms — seeded once with the same 23 defaults, editable on the ordinary Listings ▸ Amenities screen, with archive URLs at /amenity/<slug>/. NOT to be confused with the fork's other "Property features" control — the repeater of up to eight icon+label chips that print on the listing CARD (ListingCard::FEATURES_META). That one is per-listing free text with an icon; this one is the site's shared vocabulary. Two things come out of registering it properly: - the listing form's Amenities TILE (Ppt_rt\ListingTiles::amenitiesTile) drives the SHARED category-picker modal, the same control Category uses; - the search sidebar gains an Amenities facet for free — Admin\Search::taxonomyFilters() picks up every show_ui listing taxonomy, and the template's generic $taxField() renders it with live counts. Real Estate only: registered from inc/_rt/, which Theme::boot() only wires under the realestate profile.
PropertyCategories PropertyCategories.php
The Real Estate theme's shipped CATEGORIES. Category is the site's main browse axis — the tiles on the homepage, the /category/ archives, the Category facet in search, the chip on every card. A property site that installs with an empty category list has nothing to browse until someone types ten terms in by hand, so the theme ships its own vocabulary and seeds it on first run, exactly as the property vocabularies do (PropertyDwelling / PropertyAmenities / PropertyStatus). This does NOT register a taxonomy: categories are the shared listing_category (Ppt\PostTypes\Listings::TAX_CATEGORY) that every profile uses. Only the seed set is Real-Estate-specific, so it lives in the fork and is wired with the rest of inc/_rt/. The seed is one-shot AND empty-only: a site that already has categories — one the owner curated, or one carried over from another profile — is left alone. See seed(). Some names appear here AND in PropertyDwelling ("Bedsit", "Annexe", "Room in a Share"). That is deliberate, not a duplicate: the category is the section of the site a listing is filed under, the type is what the property IS, and a listing in "Shared Houses" can be an "En-Suite Room".
PropertyConfig PropertyConfig.php
Configuration — how big the property is, in bedrooms. The size axis PropertyDwelling deliberately leaves out. It exists as a taxonomy rather than only as the ppt_rt_beds number because it wants an archive URL of its own: /beds/2-bed/ is a page worth ranking, "beds=2" in a query string is not. NOBODY FILLS THIS IN. The listing form has always had a Bedrooms number field, and for a while it also had a Configuration tile offering "3 Bedrooms" — two controls for one fact, which can disagree. So the number is the only thing anyone types and the term is derived from it on save (self::syncFromBeds), exactly as the Features repeater was folded into Amenities. It is a bedroom COUNT and nothing else. "Room Only", "Shared Room" and "Studio" used to live here too and were removed: they are kinds of dwelling, not counts, and PropertyDwelling already answers that with "Room in a Share", "Bedsit" and "Studio Apartment". Keeping them here is how this vocabulary drifts back into being a second Property type.
PropertyFacing PropertyFacing.php
Facing — which way the property points. Listed in the earlier field spec as plain meta, and promoted here on reflection: it is a closed list of eight values that people genuinely browse by, which is the test for a taxonomy rather than a stored number. In India it is a Vastu question and a hard filter; in northern Europe it is a light question and sells a south-facing garden at a premium. Either way "east-facing flats in Thane" is a page, not a query string. Single-answer. The entrance faces one way, and a listing claiming four is a listing nobody believes.
PropertyFurnishing PropertyFurnishing.php
Furnishing — what comes with the property. A top-three filter on every rental portal and the one a tenant screens on before price, because it decides whether they need a van. Single-answer. "Semi-furnished" and "Part-furnished" are the same thing in different markets (India and the UK respectively). Both ship, because a site owner should find their own word in the list rather than have to add it — and a site only ever surfaces the terms its own listings actually use.
PropertyLetting PropertyLetting.php
Letting terms — what the CONTRACT includes, as distinct from what the property has. "Bills included" and "No deposit" are the two things a student actually filters on, and neither is an amenity: they are terms of the tenancy, true of the agreement rather than the building. Putting them in the amenity list would repeat the mistake the Features repeater made — one vocabulary holding two different kinds of fact, so neither can be filtered honestly. "No deposit" sitting between Microwave and Parking on a property card is a category error a reader notices even if they cannot name it. Multi-answer: a let can be bills-included AND deposit-free AND accept pets. Only meaningful on a rental. The tile hides itself when the listing's Status is a sale (see the status watcher in ListingTiles::assets()), the same way the Bedrooms field hides for an office.
PropertyOutlook PropertyOutlook.php
Outlook — what the property looks out on. 99acres calls this "Overlooking" and lists it right under the floor number, because on an identical pair of flats it is most of the price difference. It is also one of the few genuinely browsable attributes a portal has: "park-facing flats" is a search people make, and /outlook/park/ is a page worth owning. The one MULTI-answer vocabulary in this set — a corner flat overlooks the park AND the main road, and forcing a choice would lose the second, which is the one the buyer wanted to know about. So no admin column: six comma-separated values per row is not a table anybody can scan.
PropertyStage PropertyStage.php
Stage — how far through construction the property is. Distinct from PropertyStatus, which is about the SALE (For Sale, Under Offer, Sold). This is about the BUILDING: a flat can be For Sale and Under Construction at once, and the two answers belong in different columns. It is also what the fork's New Build & Off-Plan designs (Blueprint, Cornerstone, Skyline) have been dressing without any data behind it — "New Launch" and "Ready to Move" were words in a mockup rather than a filter a buyer could use. Ordered as a timeline, earliest first, which is why the picker reads by term_order rather than alphabetically.
PropertyStatus PropertyStatus.php
Property status — For Sale / To Let / Under Offer / Sold …, as a real TAXONOMY. The status used to be a <select> in the "Property details" card, stored as a key in ppt_rt_status. As a taxonomy it gets its own tile on the listing form (the same picker Category uses), an admin screen where the vocabulary can be renamed for a lettings-only or auction-only site, a browsable /property-status/<slug>/ archive, and a search facet — none of which a hard-coded select can offer. THE TERM IS THE SOURCE OF TRUTH, and ppt_rt_status is kept in step behind it. The meta is what the listing card badge, the sidebar Status row and the demo seeder all read, and mirroring the slug into it means none of that had to change: a term whose slug is one of the six known keys writes that key; anything the owner adds later writes nothing, so a renderer keyed on the six built-ins simply shows no badge rather than an unknown one. ONE status per listing — the tile posts data-cat-multi="0". Real Estate only: registered from inc/_rt/, which Theme::boot() only wires under the realestate profile.
PropertyTenure PropertyTenure.php
Tenure — the legal basis on which the property is held. The question a serious buyer asks second, after price, and the one a portal that omits it looks amateur for omitting: a leasehold flat with eighty years left is a different asset from the freehold next door. Deliberately multi-market. Freehold / Leasehold / Share of Freehold / Commonhold are England & Wales; Fee Simple is the US; Co-operative Society and Power of Attorney are India; Feuhold is Scotland's historic term and still appears in older records. A site uses the three or four that apply to it and ignores the rest. Single-answer: a property is held one way.
PropertyVocabulary PropertyVocabulary.php
Shared machinery for the Real Estate fork's shipped vocabularies. PropertyTypes, PropertyAmenities and PropertyStatus each carry their own copy of the same three jobs — register a taxonomy on the listing post type, seed a default term list once, and hand the vocabulary to the shared category picker. That was fine at three. At nine it is eight hundred lines of the same code with different nouns in it. So the vocabularies added after those three extend this instead. It owns the behaviour; a subclass states only what makes it different — its taxonomy key, its labels, its rewrite slug, whether a listing may hold more than one of them, and the terms it ships with. Deliberately NOT retrofitted onto PropertyTypes / PropertyAmenities / PropertyStatus. Those three are live, seeded on real installs, and carry per-vocabulary quirks (PropertyTypes orders by term_order, PropertyStatus seeds from ListingCard). Rewriting working code to share a base is how a seed option gets renamed by accident and a populated site re-seeds itself. Registering with show_ui is most of the payoff: the vocabulary gets an ordinary Listings ▸ … admin screen, archive URLs at /<slug>/<term>/, and a search facet for free — Admin\Search::taxonomyFilters() picks up every show_ui listing taxonomy and the template's generic $taxField() renders it with live counts. Real Estate only: everything under inc/_rt/ is wired by Theme::boot() under the realestate profile alone.
Reviews Reviews.php
Listing reviews — built on native WordPress comments so they moderate and store like any comment, but stamped with the comment type "review" (the identifier that distinguishes them from ordinary blog comments in the admin) and a 1–5 star rating saved as comment meta. Reviews are always enabled on the listing CPT. Rendered in the single-listing "Reviews" (summary + breakdown + cards) and "Add Review" (star input + comment form) sections.
Roles Roles.php
The split portal: who this member is, and what the site is therefore FOR. A property site has two sides. Somebody puts a place on it; somebody else is looking for one. Real Estate has asked which is which since the role sign-up step was written — "Buying or renting" / "Selling or letting" / "I am an agent" — and then dropped the answer on the floor: it was stored in ppt_signup_role and read by nothing at all. Every member got the same dashboard, so a tenant who said she was looking to rent was handed a sales desk and a "create listing" button. The machinery to act on that answer already exists. Account\AccountType was built for the escort fork, where the same three people show up under different names: SOLO advertises their own, AGENCY advertises other people's, CLIENT is only here to look. It owns the sign-up flow each one gets, the wording, whether finishing sign-up seeds a listing, and what the Member Hub is for. All of that is the shape of a split portal and none of it is escort-specific — only the words were. So this class is a bridge and a dictionary, and deliberately nothing else: BRIDGE map the answer we already have (ppt_signup_role: buy/sell/trade) onto the type AccountType already understands (client/solo/agency), so no second question is asked, no migration runs, and a member who signed up last year is already answered. DICTIONARY supply the words. The two sides are LANDLORD and TENANT on a rentals design, SELLER and BUYER on a home-sales one, HOST and GUEST on holiday lets, LENDER and BIDDER on foreclosures. A design says so by implementing Designs\Contracts\HasRoles; anything that does not gets the neutral Owner / Agent / Seeker pair from self::base(). Gated to the realestate profile by Theme::boot()'s fork-gating (this is Ppt_rt*), so none of it exists on any other product.
SearchPage SearchPage.php
Server-rendered search / archive for the listing post type — the clean PPT rebuild of DT10's PPT\Custom\Search\SearchPage. Runs the normal WordPress main query (WP_Query) via official hooks (pre_get_posts + template_include) so it is SEO-friendly and every core filter applies. Contexts it owns: - keyword search (/?s=…) - the listing post-type archive (/listing/) - listing category / tag archives Filters (GET, all optional, combinable): s keyword tax-listing_category category term id price1 / price2 min / max price (numeric, price meta) sort featured | newest | oldest | rating | price_low | price_high | title paged pagination
SingleListing SingleListing.php
Single-listing page for the listing_type post type — the clean PPT rebuild of DT10's PPT\Custom\Single\SingleListing. Owns template_include for singular listings and renders its own token-styled template inside the normal main loop. No legacy $CORE / membership plumbing; standard WordPress access rules apply.
TermMeta TermMeta.php
Per-term IMAGE + ICON for the listing taxonomies. Adds two custom fields to the native WordPress term add/edit screens (Categories, Tags and any custom listing taxonomy): a media-library image picker and a swatch picker over the theme icon library. The values are stored as term meta and consumed by the live-category blocks (via Blocks\Support\Categories) so an admin can give each category a branded photo and icon instead of the auto-derived listing photo / fallback glyph. Admin-only UI; the read helpers (image()/imageId()/icon()) are safe to call anywhere. Taxonomies are resolved live from the listing post type, so new custom taxonomies get the fields automatically.
Uploads Uploads.php
Listing media uploads — the AJAX backend for the Uppy uploader on the listing editor (replacing the WordPress media frame). Uppy's XHRUpload posts one file per request to ppt_listing_upload; each becomes a normal WordPress attachment (so Media::gallery()/Media::videos() and the single-listing template keep working). Files are left unattached (post_parent = 0) until the listing form is saved, which parents the final set (ListingEditor::save()). A companion ppt_listing_media_remove deletes a freshly uploaded, not-yet- saved attachment when the user removes it in the editor, so abandoned uploads don't pile up in the media library.