← All posts

When a Directory Site Needs Custom Fields, Imports, and APIs

Where directory plugin settings end and custom data work begins - bulk imports from spreadsheets and external APIs, custom field schemas, geo search, paid listings, and CRM sync.

There’s a point in almost every serious directory project where the settings panel runs out of road.

The plugin has been doing its job - you’ve got listings, categories, search filters, maybe a submission form. Then the requirements shift. The client wants to import 4,000 listings from a Google Sheet. The listing schema needs a field that’s a structured object, not a string. Search needs to filter by distance from the user’s location. Paid listings need tiers with different expiry windows and visibility rules. The CRM needs to sync when a listing owner updates their record.

None of that is covered by settings. It’s covered by code - and the code decisions made here determine whether the directory grows cleanly or accumulates technical debt that eventually requires a rebuild.

The moment the settings panel runs out

It’s worth being specific about where the line is, because it’s not always obvious until you’re standing at it.

Settings panels handle: field types that the plugin ships (text, select, image, URL, hours of operation). Bulk operations within those fields. Display templates from the plugin’s theme layer. Integrations that the plugin’s developer has already built and tested.

What they don’t handle: field types that don’t exist in the plugin’s schema definition. Import sources the plugin has no connector for. API responses with data structures that don’t map to the plugin’s listing model. Business logic that lives upstream in another system and needs to flow into listing state.

The gap between “what the settings panel can do” and “what the project needs” is where custom data work begins.

Bulk imports: spreadsheets and external APIs

The most common import scenario we encounter is the initial data load - a client who has been maintaining their directory in a spreadsheet for years and needs to get 3,000 rows into Listora correctly.

A spreadsheet import sounds simple. In practice, the problems are:

Column mapping is never clean. The spreadsheet has “Business Type” as a column; the Listora schema has category_id. “Business Type” has free-text values accumulated over three years - 14 different spellings of “Restaurant.” The import process needs a mapping step that translates the source values to valid Listora taxonomy IDs, and a review pass that surfaces rows where the mapping is ambiguous.

Media requires special handling. Image URLs in the spreadsheet may be relative, may be hosted on a service that rate-limits fetching, or may be Google Drive share links (which require OAuth to resolve to a downloadable URL). The import can’t just pass the URL string to the sideload_image flow and hope.

Error handling needs to be row-level, not batch-level. An import that fails on row 847 and rolls back the entire batch is unusable when there are 4,000 rows. The right behavior is: import what succeeds, write a report of what failed and why, let the user fix and re-import the failing rows.

External API imports add a different set of problems. The source data model doesn’t match the listing model. Fields that are required in Listora may not exist in the source API. Rate limits on the source API mean the import has to be paced, not fired all at once. The import needs to be resumable - if it’s pulling 20,000 records from an external service and the server times out at record 12,000, it needs to pick up where it left off.

We build these imports as background jobs with a progress surface in the admin - a queue-based worker that pulls from the source, transforms, and writes in batches, with a live progress indicator and a row-level error report when it finishes.

Custom field schemas

Standard directory listings cover the obvious cases: name, description, category, location, phone, website, hours, images. Most real directories need more.

A legal directory needs specializations per attorney - bar admissions, practice area codes, court jurisdictions - that are structured objects, not free text. A restaurant directory needs menu links, cuisine taxonomy, price band, and seating capacity. A medical directory needs accepting-new-patients status, insurance networks accepted, and telehealth availability.

The options at the plugin level are: use the plugin’s built-in custom field builder (fine for simple key-value fields), use Advanced Custom Fields (works, but ACF stores data in postmeta, which brings back the scaling issues described in our earlier post on meta_query), or build a structured field schema as part of the listing data model.

The structured approach stores custom field data in a typed JSON column on the listing record - or in a linked custom table for fields that need to be filterable. The tradeoff is that you own the validation, the admin input forms, and the display templates for those fields. In exchange, the fields are part of the listing schema, queryable directly, and not subject to the postmeta JOIN overhead.

For directories where the custom fields drive search filters, this matters. A filter for “insurance network: Cigna” that runs against a TINYINT column in a structured table is fast. The same filter running against a postmeta JOIN is not.

Location-based search works out of the box for most directory plugins - up to a point. “Show listings within 10 miles of Austin, TX” is a standard feature. The edge cases are where custom work enters.

Geocoding on import. Listing addresses need to be converted to (lat, lng) pairs for geo search to work. The plugin’s built-in geocoder handles one listing at a time, triggered on save. For a bulk import of 4,000 listings, you need a background geocoder that works in batches, respects the geocoding API’s rate limits, handles failures gracefully, and doesn’t require a human to trigger it listing by listing.

Geo search at high listing counts. Haversine distance in SQL is accurate but unscoped queries at 50,000+ listings are slow even with indexes. The correct approach is a bounding-box pre-filter (reduce the candidate set to a lat/lng rectangle) followed by precise distance calculation on the reduced set. Building this into the search index - as Listora does with the listora_geo table - is a data-layer decision, not something a settings panel exposes.

Multi-location listings. A franchise directory where a single brand has 200 locations is a schema problem. Is it one listing with a locations table, or 200 listings with a brand relationship? The answer depends on how the directory is navigated (brand-first vs. location-first) and what the search intent is. There’s no one-size answer, which is why it can’t be a settings checkbox.

Paid listing logic typically starts simple: free listings are basic, paid listings get featured placement and more images. It scales up in complexity quickly.

A real paid listings implementation usually involves: multiple tiers with different feature sets, expiry windows per tier, auto-renewal with payment gateway integration, a grace period between expiry and de-featuring, admin tools to manually extend or change tier, bulk renewals, and prorate rules when an owner upgrades mid-cycle.

The plugin may handle the basic payment flow. The business logic around what happens at expiry, what state the listing enters during the grace period, how upgrades are prorated, and how the CRM is notified of tier changes - these are typically custom.

The data model for paid listings needs to store: current tier, tier start date, expiry date, payment reference, and what state the listing should enter at each transition. A status state machine - similar to what we use in Jetonomy for post moderation - is the right architecture here. The listing has a state. Events (payment, expiry, manual admin action) trigger state transitions. The state determines what the listing looks like to visitors and where it appears in search results.

CRM sync

The case for CRM sync usually looks like this: a listing owner updates their record in the directory. That update needs to propagate to the CRM the client uses for outreach. Or the reverse: a contact in the CRM gets tagged with a specific attribute and the corresponding listing should update.

This is a bidirectional sync problem, which is harder than it sounds.

The key decisions:

What is the source of truth for each field? In most setups, the directory is the source of truth for listing content (name, description, category) and the CRM is the source of truth for contact data (email, phone, status). The sync needs to respect this split and not overwrite data in either direction when the source of truth is on the other side.

How are conflicts resolved? If a listing owner updates their address in the directory and a CRM admin updates the same address in the CRM in the same five-minute window, which one wins? The answer needs to be specified, not left as undefined behavior.

How are failures handled? CRM API calls fail. The sync needs to queue failed events and retry them, not silently drop them.

We build CRM sync as a webhook-driven system where the directory emits structured events on listing state changes, and the CRM integration layer subscribes to those events. The mapping between directory fields and CRM fields is configurable without code changes. Failed webhook deliveries queue for retry with exponential backoff.

Where plugin settings end and custom work begins - summarized

ScenarioPlugin settings cover itCustom data/API work needed
Import 50 listings from a CSVOften yesFor 3,000+ rows with error handling
Custom field: text, URL, imageYesFor structured objects or filterable JSON
Geo search by radiusYesGeocoding at import scale; multi-location schemas
Basic paid/free listing tiersUsually yesProrate logic, grace periods, CRM tier sync
Integration with one known CRMIf the plugin ships itFor any CRM outside the plugin’s connector list
External API syncNoAlways custom

This is the kind of scope that comes up in most serious directory projects past the initial setup. The plugin gives you the foundation. The custom data and API layer is what makes it fit the actual business requirements.

Our plugin and API development work at Wbcom Designs covers exactly these scenarios - custom field schemas, bulk import pipelines, geo layers, paid listing state machines, and CRM integrations built on top of Listora’s core. If you’re at the point where the settings panel isn’t enough, that’s the conversation to have.