How TableCrafter Works
TableCrafter turns any remote or local data source into a responsive, crawlable WordPress table using a four-stage pipeline: fetch the source, cache it with stale-while-revalidate, render the table server-side for the first paint, then hydrate it in the browser for sorting, search, and export. No custom database tables are ever created.
The pipeline at a glance
Every table you place with the [tablecrafter] shortcode (or the tablecrafter/data-table block) flows through the same four stages. Understanding them explains why the first paint is fast, why search engines see real rows, and why your data stays at its source.
- Fetch source — TableCrafter reads the
sourceURL. Local files are read straight from disk; remote JSON is fetched over cURL; Google Sheets and.csvURLs are parsed into rows. - Cache (SWR) — The parsed data and rendered HTML are stored as WordPress transients. Stale entries are served instantly while a background refresh runs.
- Server-side render — On a cache miss, PHP builds a complete
<table class="tc-table">so the page ships with real, indexable rows in the initial HTML. - Client hydrate — The frontend script attaches sorting, search, pagination, filtering, and export to the server-rendered DOM without re-fetching or re-drawing it.
TableCrafter is a presentation layer, not a data store. It reads from your source and caches the result — it never copies your data into a custom database table.
Stage 1 — Fetching the source
The source attribute drives everything. TableCrafter inspects the URL and chooses the right reader inside fetch_data_from_source():
| Source type | How it is read |
|---|---|
| Local file (same site or plugin URL) | Resolved to an on-disk path and read directly — no HTTP loopback. Path traversal is blocked by a realpath whitelist limited to ABSPATH, WP_CONTENT_DIR, and the plugin folder. |
Google Sheet (docs.google.com/spreadsheets) | Detected by pattern and parsed via the CSV source reader. |
CSV URL (ends in .csv) | Fetched and parsed into an array of row objects. |
| Remote JSON API | Fetched over cURL with SSL verification on, a 30s timeout, and a 10s connect timeout, then decoded. |
Remote URLs first pass an SSRF guard (is_safe_url()) that blocks local and private IP ranges. If the source returns a non-200 status or invalid JSON, the fetch returns a WP_Error that becomes a friendly error state downstream.
When your JSON wraps the rows in a parent object (for example {"results":{"items":[...]}}), point the root attribute at the array path with dot notation: root="results.items".
Stage 2 — Caching with stale-while-revalidate
TableCrafter keeps two transient layers, both keyed by content so different configurations never collide:
- Data cache (
tc_cache_prefix) — the parsed rows, keyed bymd5(source + version). - HTML cache (
tc_html_prefix) — the fully rendered table plus its embedded data payload, keyed bymd5of the source and the rendering attributes (include,exclude,search,filters,export,per_page,sort, and the plugin version).
Both transients live for one hour (HOUR_IN_SECONDS). The stale-while-revalidate behavior kicks in earlier: once a cached entry is older than the 5-minute stale threshold, the visitor is still served the cached HTML immediately, and a single background event (tc_refresh_single_source) is scheduled to rebuild it. Visitors never wait on a slow upstream source.
// Stale-while-revalidate, simplified from render_table()
if ( time() - $timestamp > ( 5 * MINUTE_IN_SECONDS ) ) {
// Serve the stale HTML now, refresh in the background
wp_schedule_single_event( time(), 'tc_refresh_single_source', array( $atts ) );
}
An hourly cron job (tc_refresher_cron) also warms every URL TableCrafter has seen, so popular tables are usually pre-built before anyone requests them. Caches clear automatically on a version bump, and you can purge them manually:
wp tablecrafter clear-cache # remove every TableCrafter transient
wp tablecrafter warm-cache # pre-fetch all tracked source URLs
Stage 3 — Server-side render
On a cache miss, fetch_and_render_php() builds the table in PHP. It applies your include/exclude column choices (including key:Alias header renaming), optional sort (column:direction), and emits semantic, accessible markup:
- A
<table class="tc-table">with sortable headers carryingclass="tc-sortable",tabindex="0",data-field, and anaria-sortstate. - Each cell tagged with a
data-tc-labelattribute, which powers the responsive mobile card view. - Smart value formatting with XSS escaping — booleans become
Yes/Nobadges, ISO dates become<time>elements, emails and safe http(s) URLs become links, and images render as lazy-loaded thumbnails.
The finished HTML is wrapped in a container that carries every setting as a data attribute and is marked data-ssr="true", with the row data embedded as inline JSON for the hydration step:
<div id="tc-..." class="tablecrafter-container"
data-source="https://api.example.com/data.json"
data-search="true" data-ssr="true">
<table class="tc-table"> ... </table>
<script type="application/json" class="tc-initial-data">[ ... ]</script>
</div>
If a logged-in administrator hits a fetch error, an inline TableCrafter Setup Guide with troubleshooting tips is shown instead; regular visitors see a quiet .tc-error-state message.
Stage 4 — Client hydration
When the page loads, frontend.js finds every .tablecrafter-container on DOMContentLoaded and constructs a TableCrafter instance for each. Because the container is flagged data-ssr="true" and already contains a rendered .tc-table, the library hydrates rather than redraws:
- It parses the embedded
.tc-initial-dataJSON into its working dataset — no extra network round trip. hydrateListeners()attaches click and keyboard (Enter/Space) handlers to the existingth.tc-sortableheaders, then flipsdata-ssrto"false".- Interactive UI — global search, filters, pagination, and export controls — is wired up based on the container's data attributes.
Interactive features that need fresh data (live search across all rows, refresh) go through a nonce-protected AJAX proxy, tc_proxy_fetch, which reuses the same cache and SSRF protections as the server render. The library also emits DOM events you can listen for:
| Event | Fires when |
|---|---|
| tablecrafter:cardTap | A responsive card is tapped on mobile. |
| tablecrafter:cardView | A row's card view is opened. |
| tablecrafter:cardEdit | A row's edit action is triggered. |
Why zero-DB matters
TableCrafter never registers a custom post type or a custom database table. The only thing it writes is transients — WordPress's standard, self-expiring cache — plus a small tc_tracked_urls option for cache warming. The practical consequences:
- Your data stays canonical. The source (Sheet, API, CSV) remains the single source of truth; TableCrafter just mirrors it on a TTL.
- Clean uninstall. Removing the plugin leaves no orphaned tables behind — clearing the cache disposes of everything it created.
- Cheap reads. The hot path is a single
get_transient()call, so tables render even when the upstream source is slow or briefly offline.
A complete example
This shortcode renders a paginated, searchable product table, shows only three columns (with a friendly header alias), pre-sorts by price descending, enables export, and refreshes every five minutes:
[tablecrafter source="https://api.example.com/products.json"
root="data.items"
include="name, price, stock:In Stock"
sort="price:desc"
per_page="25"
search="true"
export="true"
auto_refresh="true"]
With export="true", the table gains a download menu. Exports are produced by the canonical export handler (includes/class-tc-export-handler.php), which generates a genuine OOXML .xlsx workbook via ZipArchive and a real PDF — not a CSV renamed with a different extension.
Where to set it up in wp-admin
Open WordPress Admin → TableCrafter. The dashboard offers one-click Quick Start demos (User Directory, Product Inventory, Sales Metrics, Employee CSV, Google Sheet, Airtable), a settings panel where you paste a Data Source URL, upload a CSV/JSON file, or connect a Sheet, and a Live Preview pane. When the preview looks right, copy the generated shortcode from the Usage card and paste it into any post, page, or block. In the block editor, search for the tablecrafter/data-table block to configure the same options visually.
A change to your include, exclude, sort, or per_page values produces a new cache key, so the new layout appears on the next render. If you change the upstream data itself and want it live immediately, run wp tablecrafter clear-cache rather than waiting for the hourly refresh.
Next steps: see shortcode-reference.html for every attribute in detail, and caching-and-performance.html to tune the stale-while-revalidate behavior and cron warming.