Skip to main content

Directory

25 min read

Directory

Shared across every PremiumPress product. 33 classes in inc/Directory.

AlertsBox AlertsBox.php​

The search rail's email signup box — "New jobs by email". A filter slot that filters nothing: it sits in the filter order like any other facet, carries the same admin on/off toggle and drag handle, and draws an email capture card at the foot of the rail. Added 2026-09-13, user-directed. WHERE THE EMAIL GOES. Straight to the existing ppt_subscribe endpoint ({@see \Ppt\Subscribers\Subscribers}), so it inherits double opt-in, the disposable-domain and MX checks, the honeypot, the unsubscribe link, the site-wide "Enable subscriptions" switch and the Subscribers admin table rather than opening a second door onto the same list. source is 'search-rail', so an owner can see in that table where a signup came from. WHAT IT THEREFORE MUST NOT SAY. Nothing binds a signup to the query the visitor is looking at — this is the newsletter list, not a per-search alert — so the shipped copy promises new {listings}, never "jobs matching this search". The wording is the admin's to change (PremiumPress ▸ Search), and if a site wants to promise more than the list delivers that is their call; the theme will not ship the promise. A real search-bound alert exists for logged-in members on the Job Board ({@see \Ppt_jb\JobAlerts}) and is a different feature. ══ THE TRAP THIS BOX IS BUILT AROUND ═══════════════════════════════════════════ The rail renders INSIDE the search filter <form>. A nested <form> is invalid HTML — the browser throws the inner tag away and KEEPS its fields, which would post the visitor's email address with every filter submission. So: a div, NO name attribute anywhere in here, and the values read by script. Two consequences worth keeping: - the button is type="button", or it would submit the filter form; and - Enter inside the email box is swallowed, for the same reason. A visitor typing an address and pressing Return must subscribe, not re-run the search. Do not "tidy" this into a form. The Coupon fork learned it first ({@see \Ppt\ppt_blocks_cp\sidebar\Alerts}). The handler is delegated from document, so the AJAX filter refresh can re-render the rail without rebinding.

API
available()defaults()copy()render()resetOnce()
Directory/Filters/AlertsBox.php

DefaultFields DefaultFields.php​

The Directory theme's factory custom fields — the eight facts a visitor wants about ANY local business, seeded once on a fresh install. Directory shipped no default fields at all, so `

Fields::factorySeeds()was empty, the "Reset to defaults" control was hidden (it had nothing to restore), and a new site's listing pages had no Details block until an admin invented fields themselves. That gap is also how a demo site ends up advertising whatever fields happened to be typed in — a café listing reporting an escort profile's attributes. DELIBERATELY NICHE-AGNOSTIC. Directory is one product sold to restaurants, salons, gyms, trades and everything else, so every field here has to read sensibly for a plumber AND a bistro. That rules out Cuisine, Dress code and Outdoor seating however well they would suit the restaurant showroom — a niche pack can add those through the sameppt_default_fields` filter.

API
register()defaultFields()seeds()definitions()seedFields()
inc/Directory/DefaultFields.php

DemoAccounts DemoAccounts.php​

Directory — what the typed DEMO members walk into on top of the shared furnishing (Account\DemoFurnish: favourites, follow, inbox thread, review). the BUSINESS OWNER (AccountType::SOLO) three seeded listings (DemoLogin lends them — the count is raised here from one); the CUSTOMER (AccountType::CLIENT) has favourited them, follows the owner, has left a review and has an enquiry thread open. Self-gated on the Directory profile (inc/Directory ships in every product). Clone of Ppt_cd\DemoAccounts (2026-09-20).

API
register()lendCount()
inc/Directory/DemoAccounts.php

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.

API
register()metaKey()schedule()unschedule()onTransition()applyLifetime()runCheck()timestamp()remaining()dateLabel()
inc/Directory/Expiry.php

FilterIcons FilterIcons.php​

Ppt\Directory\FilterIcons — draws the admin's chosen icon inside a search filter. ONE RENDER PASS, the convention this theme already uses for anything that has to touch every block without editing every block (Blocks\Support\HowLinks, the live-items overlay, card-hitareas): the search template renders a filter into a buffer and hands the fragment here, so all ~40 field renderers — and every layout that dispatches them (top band, sidebar, three-column, the map bar) — are covered from a single call site. Nothing is duplicated per fork. The icon is chosen per filter in PremiumPress > Search (Admin\Search::filterIcon), from the shared Ppt\Support\Icons library. WHY THE MARKUP IS REWRITTEN RATHER THAN PASSED DOWN: a <select> cannot contain an icon, so the glyph has to be positioned OVER the control. Anchoring it to the cell fails — in the sidebar layout a visible label sits above the control, so "centred in the cell" is not "centred in the field". Anchoring it to a wrapper that hugs the control works at any field height, in any layout, with no measurement.

API
inject()css()
inc/Directory/FilterIcons.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)

API
profileDefaults()defaultStyle()styles()locked()current()isFullSpan()render()css()
inc/Directory/Gallery.php

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.

API
register()geocode()reverse()pendingCount()ajaxBatch()assets()box()
inc/Directory/Geocoder.php

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.

API
register()favIds()favHas()favToggle()favListingIds()ajaxFav()ajaxReport()ajaxClaim()assets()favButton()reportButton()claimButton()
inc/Directory/ListingActions.php

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), badges (list), online (bool), 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. badges (the owner's own coloured pills — see Content\ProfileBadges) and online (the derived "they are here right now" state) only reach the card on the product lines where a listing IS a person; ThemeProfiles::supportsProfileBadges() gates both, and on those lines badge is drawn as a diagonal corner banner instead of the usual pill.

API
fromPost()isOnline()fromSample()render()
inc/Directory/ListingCard.php

ListingClaim ListingClaim.php​

"Claim this business" — the flow that hands an unclaimed directory listing to the member who actually runs it. WHY THIS EXISTS: a directory is usually seeded from a bulk import, and {@see Ppt\Porting\Importer} authors every imported row to the ADMIN who ran the import. Those listings describe real businesses that have no account here, and the owner's first instinct on finding their own page is to ask for control of it. Until now the theme had no answer — two designs (_dt\Sterling, _dt\Locale) even advertise "Claim your listing" in their CTA copy, with nothing behind it. WHAT "UNCLAIMED" MEANS: ownership in this theme is post_author ({@see Ppt\Directory\ListingEditor::canEdit} gates editing on it), so a listing is unclaimed when its author is STAFF rather than a member — tested with edit_others_posts, which administrators and editors hold and members never do. That is exactly the population the importer creates, and it needs no new column. THE DEMO GUARD IS NOT OPTIONAL. {@see Ppt\Tools\SampleData::ownerPool()} spreads seeded listings across non-admin members, but FALLS BACK TO THE CURRENT ADMIN when the site has no members yet. So the staff-author rule alone swings between two wrong answers on a demo site: zero claim buttons once members exist, and a claim button on every one of thousands of seeded rows on a fresh install. Demo dressing must never advertise a claim, so anything carrying the canonical demo marker ({@see Ppt\Support\Demo}) is excluded outright. SCOPE: Directory only, and that is free rather than enforced — Theme::boot() drops every Ppt\Directory\* service when a product fork is active, so the button and its endpoint simply do not exist on the twenty forks. That is deliberate: a _da or _es listing is a PERSON's profile, where "claim this business" is meaningless and would be a safeguarding problem, not a feature. STORAGE: a claim is a comment on the listing, carrying our own comment type and a custom approval status — the same trick {@see Ppt\Directory\ListingActions} uses for abuse reports, which keeps claims out of every public comment query (those match comment_approved = '1') and out of the normal moderation tabs, so they surface only in the admin's Claims tab. Approving one MOVES post_author and leaves an audit trail on the listing, so a mistaken approval can be traced and undone. A static helper, not a Service: called from ListingActions (front end) and Admin\Comments (review queue), so it is never registered and never subject to boot()'s fork-gating. Mirrors {@see Ppt\Directory\ListingReport} for the same reason.

API
claimable()inDemo()canClaim()hasPending()queueCount()submit()approve()deny()
inc/Directory/ListingClaim.php

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.

API
register()newUrl()handleNew()submissionsOpen()memberCanAdd()canEdit()adminEditUrl()memberEditUrl()applicableFields()locationKeys()coordKeys()contactKeys()contactFieldsOn()mapPickerReady()mapConfig()locationFields()socialNetworks()socialValue()socialIcon()hoursDays()hoursValue()normTime()money()untitledTitle()
inc/Directory/ListingEditor.php

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.

API
items()
inc/Directory/ListingFaq.php

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.

API
has()isOpenNow()renderSidebar()
inc/Directory/ListingHours.php

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().

API
address()coords()has()render()
inc/Directory/ListingLocation.php

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) Leaflet is self-hosted (assets/vendor/leaflet/1.9.4, see enqueueLeaflet()); the OSM tiles, Nominatim and the Google embed are the owner-approved map exception.

API
render()enqueueLeaflet()lazyInit()
inc/Directory/ListingMap.php

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\Directory\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.

API
plans()demoPlans()has()lowestPrice()normalise()render()
inc/Directory/ListingPricing.php

ListingReport ListingReport.php​

"Report this listing" admin notice — the shared sender behind every fork's report action (Ppt\Directory\ListingActions and its fourteen per-fork copies all call here). Each fork used to carry a byte-identical block that hand-rolled the subject and body and posted them straight to wp_mail(), which meant fifteen copies of wording no admin could see or edit, none of them carrying the site's From name or the shared email header/footer. This owns it once, through the admin-editable template system (listing_reported), so a change lands everywhere at once. A static helper, not a Service — called at report time from the fork classes, so it is never registered and never subject to boot()'s fork-gating. Mirrors the way Ppt\Directory\Qanda owns the shared Q&A thread for the same reason.

API
notifyAdmin()
inc/Directory/ListingReport.php

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.

API
supported()catalog()config()order()enabled()orderedAmong()sanitize()
inc/Directory/ListingSections.php

ListingTiles ListingTiles.php​

The tile row at the top of the Directory listing form: Category and Business hours. Both were stacked fields a long way apart — Category a chip-and-button field under the title, Business hours a whole card further down the form that a member had to scroll past everything else to reach. They are the same kind of decision ("what is this listing, in one glance"), so they now read as one row of tiles: icon, label, and either the chips/summary that are set or the word "Select". This is the control the Coupon theme (Ppt_cp\ListingTiles) and the Escort theme (Ppt_es\ProfileSpecs) already use, brought to the base Directory form: Category opens the SHARED category picker modal (assets/js/category-picker.js) — searchable, with a Clear All / Continue footer. Business hours pulls the form's OWN hours card (#ppt-dt-hours-card) into a modal. The card is MOVED, never copied, so it stays inside the <form> and its inputs submit exactly as they did in place — there is never a second copy of hours[…] on the page.

API
hasHours()categoryTile()hoursTile()hoursSummary()assets()
inc/Directory/ListingTiles.php

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).

API
register()table()install()shared()schedulePrune()prune()countsReady()backfillCounts()bump()viewerHash()firstViewThisHour()log()lifetimeMany()lifetime()orderSql()maybeRecord()isBot()total()totalsFor()totalsSince()countSince()series()recentViewers()ajaxData()
inc/Directory/ListingViews.php

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.

API
primary()gallery()videos()media()counts()
inc/Directory/Media.php

Qanda Qanda.php​

Listing Q&A thread — the shared renderer behind every fork's member comments (Ppt_fm\Comments, Ppt_ct\Comments, Ppt_at\Comments, Ppt_football\Comments all delegate here). One place owns the whole affordance so the four forks stay identical without copy-paste: • Threaded replies — top-level questions with a nested list of answers below each. Any signed-in member can reply (a reply from the listing owner or a moderator is badged). Replies are ordinary WordPress comments carrying the 'ppt_qa' type and a comment_parent, so they moderate and store like any other; the fork's preprocess_comment stamp (ppt_qa=1 hidden field) tags them. • Up / down voting — a thumbs pair with live counts on every question and reply, so members surface the useful ones. Storage + the AJAX endpoint live in Ppt\Account\CommentVotes; this file only renders the bar and reads counts. Demo / preview listings (no real comments yet) fall back to the deterministic sample Q&A from Ppt\Content\DemoContent::questions(), grouped into question → answer threads with seeded (non-interactive) vote counts, so the section reads fully populated before a single real comment exists. A static helper, not a Service — called at render time from the fork classes, so it is never registered and never subject to boot()'s fork-gating.

API
count()autoApprove()suppressCoreNotify()notifyNew()renderList()renderForm()
inc/Directory/Qanda.php

Rail Rail.php​

Rail — the search page's filter controls, as a component any page can draw. This is the theme's ONE filter rail. It was the $ppt_filters closure inside inc/Directory/templates/search.php from the day that template was written, which was fine while the search page was the only page that had filters. It is not any more: a Job Board design's HOME page draws a filter column beside its job list, and drawing it a second time by hand is how the three Classic Jobs designs ended up with a rail of tick-boxes that looked like filters and filtered nothing. Every facet renderer, every fork gate and every rail style now lives here, and both callers render the same rail. ══ WHAT MOVED, AND WHAT DID NOT ══════════════════════════════════════════════════ {@see self::render()} is that closure's body, moved VERBATIM — same code, same order, same indentation (it is left at the closure's level on purpose, so the move can be diffed against the .prefilterrail.bak copy of the template line for line rather than read as a rewrite). The 26 values it used to use are now the $ctx array, unpacked into the very same variable names at the top of the method, so nothing below the unpack had to change. What this does NOT own is the <form> around it. The search template wraps the rail in the form that also carries its results grid; a home page wraps it in one of its own pointed at the search page. Both get the rail's hidden s field (see $pptKwBand in context()), which is what makes a GET submit land on the search results rather than back where it started. ══ CALLING IT FROM SOMEWHERE THAT IS NOT THE SEARCH PAGE ═════════════════════════ Rail::render(false, Rail::context()); // stacked sidebar rail {@see self::context()} builds the 26 values from the same Admin\Search / SearchPage calls the template makes, with the handful that only mean something on a results page (the escort top band, the result total, the preview flag) resolved to their off-page values. Pass overrides as its argument.

API
aliasForkClasses()saveFacetBundle()saveSearchButton()css()js()panelClass()bodyClass()afterPanel()context()render()
Directory/Filters/Rail.php

RelatedListings RelatedListings.php​

"Related listings" — a row of up to four extra listings shown as FULL listing cards (the same canonical search-result card) underneath the main single listing. Each card links through to that listing. In demo / preview mode it's populated from the previewed design's own on-brand cards (each carrying a virtual demo single URL), so a showroom single page always reads as rich; on a live site it collects other published listings in the same category, and hides itself when there aren't enough to be worthwhile. The card itself comes from the active fork's ListingCard (Theme::forkClass), so the strip always matches that theme's search cards exactly — one shared class serves every fork template that doesn't override it (_ll has its own).

API
count()items()render()css()
inc/Directory/RelatedListings.php

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.

API
register()open()stampType()saveRating()onCommentChange()onStatusChange()syncPostRating()count()all()stats()stars()renderSummary()renderList()renderForm()
inc/Directory/Reviews.php

Roles Roles.php​

Customer or Business owner: who this member is on the Directory profile. A directory has two sides. A BUSINESS OWNER lists their business; a CUSTOMER browses, saves favourites, leaves reviews and sends enquiries. Sign-up asks which in its role step ("browse" / "list") and stores the answer in ppt_signup_role; this bridges that answer onto the type Account\AccountType understands (buy → CLIENT, sell → SOLO) and supplies the words — the Car Dealer bridge (Ppt_cd\Roles), cloned 2026-09-20 so the home-demo can offer a one-click "Customer login" and "Business owner login", the hub can show the badge, and a customer's hub can drop the Selling half (hidesSelling(), the auction rule). NOT fork-gated: the Directory profile runs on the shared core (Theme::activeFork() is ''), and inc/Directory ships in every product — so register() gates itself on the profile and every other product ignores this class entirely.

API
register()profiles()typeForUser()typeCurrent()toType()options()shortLabel()typeOf()isBuyer()hidesSelling()accountBadge()
inc/Directory/Roles.php

SearchAjax SearchAjax.php​

In-place search results: the server half. The search pages are plain GET forms — every filter, sort, chip and pager click is a full navigation, and on a busy site the query alone can run past a second (the heading band prints the number). That leaves the visitor staring at a dead page, then a white repaint and a jump back to the top. So the results region is swapped in place instead: search-ajax.js paints skeleton cards the instant a control is used, re-requests THE SAME URL with ppt_partial=1 and drops the real markup in when it lands. The partial deliberately re-enters the ordinary page: same template, same query, same closures — this class only tells the template to render the results fragment on its own and hand it back as JSON. Nothing about how a result is produced is duplicated here, so a fork that changes its cards, facets or sorting changes the AJAX response for free.

API
isPartial()send()cleanUrl()enabled()
inc/Directory/SearchAjax.php

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 | popular | newest | oldest | rating | price_low | price_high | title paged pagination

API
register()shapeQuery()applyCappedTotal()wasCapped()totalLabel()nearCoords()radiusKm()distanceFrom()distanceLabel()radiusClausesMain()radiusClauses()featuredOrderClauses()popularOrderClauses()isListingContext()postType()isMapTakeover()categoryTax()terms()taxQueryFromRequest()listingTaxonomies()favOnly()favIds()hasActiveFilters()matchingIds()
inc/Directory/SearchPage.php

SectionPlacement SectionPlacement.php​

The listing-page board: which cards a profile's single listing page is made of, which column each sits in, in what order, and which are switched off. Design ▸ Listing Page Design draws the page as a two-column board (main | sidebar) of wireframe cards; the single listing template draws each card where it was put. The optional sections' order and on/off stay with ListingSections — this adds the column, the order of EVERY card (core ones included) and the core cards' switches. Stored beside ListingSections' own keys, under the same design-option entry: listing_sections.place = { key: 'main'|'side' } listing_sections.board = [ every card, top to bottom ] listing_sections.hidden = [ switchable core cards switched off ] A card never placed keeps the column the page has always drawn it in, so a site that never opens the board renders exactly as before. WHICH PROFILES. Every profile whose single listing renders through the shared inc/Directory/templates/single.php (active()). The profiles with a page of their own (coupon, shop, classifieds, dating, video, escort, cashback, and compare's merchant page) keep their fixed layout until each page is converted; the two with no listing page at all (search off: ai, privatelet) have nothing to lay out. The board is built per profile from what that page can actually draw (cards()): a fork's partials become cards named for that fork (a bid box, a licence box, a spec strip …), a section the page never draws is not offered, and a switch that only shapes something inside another card (the photo theme's Licensing) stays a plain switch under the board.

API
topCards()galleryPins()ownTemplate()active()directoryBox()cards()looseSwitches()isCore()isSwitchable()shown()sanitizeHidden()place()sequence()inZone()phoneOrder()pinned()sanitizeBoard()sanitize()
inc/Directory/SectionPlacement.php

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.

API
register()demoBookingProvider()template()related()
inc/Directory/SingleListing.php

Sold Sold.php​

"Sold" status — a shared, cross-theme concept: a listing/item can be marked as sold, which shows a "Sold" ribbon on its card and single page, and (when the admin opts in) drops it from search results. Deliberately thin and fork-agnostic, mirroring the other shared directory helpers: the state is a single post-meta flag (self::META), toggled from the admin and member listing editors. Any fork can adopt it — the visible ribbon is wired per fork (see each fork's ListingCard + the global single template), while the search-hide and the save both run from here so they behave the same everywhere. self::applies() gates which profiles currently expose the toggle.

API
isSold()set()saveFromForm()applies()label()hideFromSearch()searchMetaClause()
inc/Directory/Sold.php

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.

API
register()hookTaxonomies()taxonomies()imageId()image()icon()isPinned()setPinned()pinnedFirst()assets()addFields()editFields()enqueuePanel()panel()badge()imageControl()iconControl()save()css()
inc/Directory/TermMeta.php

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.

API
register()userOwnsAttachment()attachmentBelongsTo()imageMimes()videoMimes()ajaxUpload()ajaxRemove()ajaxPoster()storePoster()ajaxPersist()
inc/Directory/Uploads.php