How to build a searchable directory with Gravity Forms
Member directories, event attendee lists, recipe collections, agent rosters. The public read-only display pattern in Gravity Tables, with caching and search-friendly URLs.
Most of our guides cover the editing side of Gravity Tables. This one covers the opposite: a polished, public, read-only directory page where visitors can search, sort, and browse entries.
What you’ll build#
A page at /directory that, for any visitor (logged in or not), shows:
- A searchable, sortable table of approved entries
- Filter chips for category, location, tag, etc.
- Mobile card layout for phones
- Pagination or “load more” for long lists
- Read-only, no editing, no audit log, no Add Entry button
- Friendly URLs that survive caching
Common shapes this fits:
- Member / staff / agent directory
- Public event attendee list
- Recipe or product catalogue
- Speaker roster for a conference
- Open submissions board (writing prompts, project ideas)
The shortcode#
For most directories, this is the whole answer:
[gravity_table id="42"
filter="status:approved"
filters="category,tag,location"
per_page="24"
mobile_layout="cards"
sort="name:asc"]
What each parameter does:
filter="status:approved", hides any entries still in moderationfilters="category,tag,location", adds filter widgets for users to narrow by these columnsper_page="24", comfortable density for desktop grid; falls back to a stack on mobilemobile_layout="cards", phones get tap-friendly card layout instead of a horizontal-scrolling tablesort="name:asc", alphabetical default; users can re-sort by clicking column headers
Moderation#
Public directories almost always need moderation. The pattern:
- Add a hidden
statusfield to your Gravity Form, default valuepending - Build a separate admin-only Gravity Tables view at
/admin/directory-moderation:
[gravity_table id="42"
filter="status:pending"
allowed_roles="moderator,administrator"
allow_edit="status,notes"
bulk="approve,delete"]
Moderators see only pending entries, can edit status to “approved” or “rejected”, and the public page picks up approved ones automatically.
What to avoid in public directories#
Don’t enable inline editing#
allow_edit="..."
A public directory is a display surface, not a form. Anyone walking by can’t edit. Skip this parameter entirely.
Don’t enable Add Entry from the directory page#
The submission form for new entries should be a separate page. Mixing display and submission on the same page is confusing for visitors and creates moderation gaps.
Don’t use filter_by_user#
That’s the customer-portal pattern, hide everyone’s entries from everyone else. Public directories show everyone’s approved entries to everyone.
Caching#
Public directories are great cache candidates because the data changes slowly (moderation cycle, not live edits).
If you’re using WP Rocket / W3 Total Cache / LiteSpeed:
- Page cache the directory page, visitors get sub-200ms TTFB
- Cache lifetime: match your moderation cadence. Hourly is generous. Daily is fine for slow-moving directories.
When a moderator approves a new entry, manually flush the directory page from the cache plugin’s UI, or call wp_cache_post_change programmatically from a gform_after_submission hook.
SEO#
A public directory should be search-engine-indexable.
Friendly anchor URLs#
Filters and pagination append URL fragments that crawlers can follow:
/directory#filter=category:speakers&page=2
Most search engines won’t index every filter combination (good, we don’t want them to), but they will follow the base URL and find every approved entry through the pagination.
Structured data#
For each row, you can add ItemList schema via the gt_render_row hook. Example for a speaker directory:
add_action('gt_render_table_after', function ($table) {
if ($table->id !== 42) return;
$entries = get_table_entries($table->id);
$items = array_map(function ($i, $e) {
return [
'@type' => 'Person',
'position' => $i + 1,
'name' => $e['name'],
'jobTitle' => $e['title'],
];
}, array_keys($entries), $entries);
echo '<script type="application/ld+json">' . json_encode([
'@context' => 'https://schema.org',
'@type' => 'ItemList',
'itemListElement' => $items,
]) . '</script>';
});
Pagination vs “Load more”#
Default is paginated (1, 2, 3 … 12). For visual / image-heavy directories (recipes, products, photo galleries), users prefer “Load more”:
[gravity_table id="42" pagination_style="load_more" per_page="12"]
Each click appends 12 more rows without a page navigation. Better for browsing, slightly worse for deep-linking (the URL doesn’t update).
Read-only with optional staff edit#
Common pattern: public visitors see a directory; logged-in staff get inline-edit capability for moderation tweaks.
[gravity_table id="42"
filter="status:approved"
allow_edit="bio,phone,office_hours"
edit_permissions="bio:staff,phone:staff,office_hours:staff"]
Visitors see read-only cells. Staff (with the staff capability) get click-to-edit on the three columns listed. The same shortcode renders both UIs based on the current user’s capability.
Real example: a speaker directory#
[gravity_table id="conference-speakers"
filter="status:approved"
filters="track,role"
include="name,role,track,bio,linkedin"
sort="name:asc"
per_page="20"
mobile_layout="cards"]
The form’s linkedin field renders as a clickable URL via Gravity Tables’ built-in URL field handling. The bio truncates at ~120 chars on the table view; mobile cards show the full bio.
Real example: a member directory with photo#
[gravity_table id="members"
filter="status:active"
filters="department,location"
include="photo,name,role,email,phone"
sort="name:asc"
per_page="32"
mobile_layout="cards"]
The photo column displays as an inline thumbnail (Gravity Tables auto-detects image URLs). On mobile cards, it becomes the card’s lead image.
Performance for large directories#
Up to ~5,000 entries, no special configuration needed. Beyond that:
- Server-side pagination is on by default; only the current page hits the client
- Index the GF entry meta fields you filter on (
status,category, etc.),wp gf entry-meta-index <field_id>if Gravity Forms ships such a CLI command, or directly via SQL - For >50,000 entries, consider splitting by year or category into multiple tables
Related#
- Quick start, first table in 5 minutes
- Filtering, full filter reference
- Hooks, extending row rendering with structured data
- Customer portal walkthrough, the per-user counterpart