The Rendering Pipeline
TableCrafter renders a complete, crawlable HTML table on the server for SEO and first paint, then progressively hydrates that same markup in the browser so search, sort, pagination, and export work without a second fetch.
Overview: two render passes, one table
Every [tablecrafter] shortcode produces output in two phases. PHP fetches your data source, builds a real <table>, and prints it inside a container element. The browser then finds that container, reads an embedded JSON copy of the data, and attaches interactive behavior to the existing DOM instead of rebuilding it. The two phases are deliberately the same table, so search engines and no-JS visitors see fully-formed content while interactive visitors get a live grid.
- Server-side render (SSR):
render_table()callsfetch_and_render_php(), which emits a<table class="tc-table">with sortable headers and labeled cells. - Transport: the table is wrapped in
<div class="tablecrafter-container" data-ssr="true">alongside a<script class="tc-initial-data">JSON payload. - Client-side hydration:
frontend.jslocates the container and constructs aTableCrafterinstance that adopts the SSR markup and wires up listeners.
Because the searchable, sortable content is present in the initial HTML response, crawlers index your table data directly. There is no client-only render path that hides rows behind JavaScript.
Phase 1 - Server-side HTML render
When the shortcode runs, attributes are normalized and the source URL is sanitized with esc_url_raw(). If a cached render is not available, fetch_and_render_php() fetches the JSON or CSV, drills into the root path if supplied, resolves the column set from include/exclude, applies any sort, and assembles the table string.
The generated markup is intentionally minimal and semantic:
<!-- Simplified SSR output from fetch_and_render_php() -->
<table class="tc-table">
<thead><tr>
<th class="tc-sortable" tabindex="0" aria-sort="none" data-field="price">Price</th>
</tr></thead>
<tbody>
<tr><td data-tc-label="Price">19.99</td></tr>
</tbody>
</table>
Three details in this markup matter to the rest of the pipeline:
th.tc-sortablecarriesdata-field(the raw data key),tabindex="0"for keyboard focus, and anaria-sortstate. If the shortcode includes asortattribute, the matching header ships witharia-sort="ascending"or"descending"already set.- Each
tdcarriesdata-tc-labelwith the human-readable column name, which the responsive card layout uses on small screens. - Header labels honor column aliasing (
include="price:Cost") and otherwise fall back to a title-cased version of the key viaformat_header_php().
Output sanitization
Before the table reaches the page it passes through sanitize_table_html(), a wp_kses() pass with an explicit allow-list. Only table structural tags plus a, img, span, time, div, strong, em, and small survive, and link protocols are limited to http, https, and mailto. This keeps rendered remote data from injecting scripts or disallowed markup.
Phase 2 - The container and data payload
The sanitized table is printed inside the hand-off container. The container's data-* attributes mirror the shortcode configuration so the JavaScript layer can reconstruct the same options without re-parsing the shortcode:
<div id="tc-..." class="tablecrafter-container"
data-source="..." data-search="false" data-filters="true"
data-export="false" data-per-page="0" data-sort=""
data-ssr="true">
<!-- sanitized <table class="tc-table"> here -->
<script type="application/json" class="tc-initial-data">[ ... ]</script>
</div>
The data-ssr="true" flag is the signal that pre-rendered content is present. The inline tc-initial-data script holds the exact dataset PHP rendered, so the browser can search and sort the full set without an immediate network round-trip.
If the source resolves but returns no usable rows, the container renders a <div class="tc-loading"> placeholder instead, and the client fetches data live. Logged-in admins additionally get a diagnostic error helper when a source fails.
Phase 3 - Client-side hydration
On DOMContentLoaded, frontend.js selects every .tablecrafter-container, reads its data-* options, and instantiates the library:
const containers = document.querySelectorAll('.tablecrafter-container');
containers.forEach(container => {
new TableCrafter('#' + id, {
data: source, responsive: true,
pagination: perPage > 0, pageSize: perPage > 0 ? perPage : 25,
globalSearch: search, filterable: filters, exportable: exportable
});
});
Inside the constructor the library finds the .tc-initial-data script, parses it into its working dataset, and checks data-ssr. When the flag is "true", it does not wipe the container. Instead it processes the data, auto-discovers columns, detects filter types, flips data-ssr to "false", and calls hydrateListeners(). That method binds interactivity onto the markup PHP already produced:
- Each
th.tc-sortablegets aclicklistener and akeydownlistener that fires on Enter or Space, both callingsort(field)using the header'sdata-field. - The full dataset from
tc-initial-databacks every subsequent search and sort, so the first interaction is instant.
A second init() runs after a 500ms timeout as a guard against script-load race conditions; the data-tcInitialized marker prevents double-initialization.
What hydration enables: search, sort, paginate
| Behavior | Trigger | Implementation |
|---|---|---|
| Global search | search="true" | Renders .tc-global-search-container > input.tc-global-search; input is debounced 300ms, resets to page 1, then re-renders the filtered set. |
| Column sort | Click / keyboard on header | sort(field) toggles asc/desc, re-sorts in memory, resets to page 1, and announces the change for screen readers. |
| Pagination | per_page > 0 | Renders .tc-pagination controls; pageSize defaults to 25 when pagination is on but no per_page is given. |
| Export | export="true" | Renders .tc-export-controls with CSV / XLSX / PDF options routed through the AJAX export endpoints. |
| Responsive cards | Narrow / touch viewport | isMobile() swaps the table for .tc-cards-container, using each cell's data-tc-label as the field label. |
SSR currently renders only the table element. Search inputs, filter rows, pagination, and export controls are created by the JavaScript layer during hydration. With JavaScript disabled, visitors still see and can read the full table, but the interactive controls will not appear.
Caching: Stale-While-Revalidate
The pipeline avoids re-fetching on every page load through a two-layer transient cache:
- HTML cache keyed as
tc_html_<md5>(the hash folds in source, include/exclude, search/filters/export flags,per_page,sort, and the plugin version) stores the rendered HTML plus the data payload for one hour. - Data cache keyed as
tc_cache_<md5>stores the raw fetched dataset.
When a cached render is served and its timestamp is older than five minutes, the request schedules an invisible background refresh via wp_schedule_single_event() on the tc_refresh_single_source hook. The visitor gets the cached (stale) table immediately while WordPress revalidates the source out of band - classic stale-while-revalidate.
Assets and the JavaScript contract
During render, register_assets() registers the script and style handles and the shortcode enqueues them:
| Handle | File | Role |
|---|---|---|
tablecrafter-lib | assets/js/tablecrafter.js | The core TableCrafter rendering and hydration engine. |
tablecrafter-frontend | assets/js/frontend.js | Bootstraps every container on the page; depends on tablecrafter-lib. |
tablecrafter-style | assets/css/tablecrafter.css | Styles for .tc-table, .tc-cards-container, pagination, search, and export UI. |
Configuration crosses to JavaScript through the localized tablecrafterData object, which carries ajaxUrl plus the nonce, exportNonce, and downloadNonce values used by the proxy-fetch and export endpoints.
Extension points and events
The hydrated instance dispatches namespaced DOM events on its container so you can react to interactions:
const el = document.querySelector('.tablecrafter-container');
el.addEventListener('tablecrafter:cardTap', e => {
console.log(e.detail.rowData);
});
Events are emitted as tablecrafter:<name> via CustomEvent (for example cardTap, cardView, and cardEdit), each carrying the relevant row data in event.detail. On the PHP side, the background-refresh action tc_refresh_single_source and the AJAX actions tc_proxy_fetch, tc_export_data, and tc_download_export are the integration seams the pipeline relies on. Configure tables from WP Admin → TableCrafter.
Lifecycle at a glance
| Stage | Runs on | Produces |
|---|---|---|
| Fetch + render | Server (PHP) | <table class="tc-table"> with sortable headers |
| Wrap + embed data | Server (PHP) | .tablecrafter-container[data-ssr="true"] + tc-initial-data |
| Cache + revalidate | Server (transients + cron) | tc_html_* cache, background refresh |
| Detect + hydrate | Browser (JS) | TableCrafter instance bound to existing DOM |
| Interact | Browser (JS) | Search, sort, paginate, export, responsive cards |
Next, see shortcode-reference.html for the full attribute list that drives both render phases, and caching-and-performance.html for tuning the stale-while-revalidate behavior.