Fieldnote Interactive
Interactive WordPress blocks built with the Interactivity API: a filterable post grid with client-side navigation, and a save-for-later reading list shared across blocks. Progressive enhancement, Block Hooks, Playwright tests and a live Playground demo.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/jdyach/fieldnote-interactive/archive/refs/heads/main.zipInteractive WordPress blocks built with the Interactivity API:
- Filterable Post Grid. Category filters and pagination that update in place using the Interactivity Router. The URL updates, the back button works, and with JavaScript turned off it is still a fully working, server-rendered filter.
- Save for Later + Reading List. A bookmark toggle on posts and a counter in the site header that share one global store, so saving anywhere updates everything instantly. The list persists in
localStorageand stays in sync across browser tabs.
The live demo spins up a real WordPress site in your browser with this plugin, the Fieldnote block theme and sample posts. Nothing to install.

How it works
Everything is rendered on the server first, then enhanced with directives. Each feature maps to specific Interactivity API concepts:
| Feature | How it is built |
|---|---|
| Filter and page without reloading | Real <a href="?fi-cat=…"> links with data-wp-on--click="actions.navigate". The action calls navigate() from @wordpress/interactivity-router, which fetches the same URL and swaps only the data-wp-router-region. |
| Router loaded only when needed | The router is pulled in with a dynamic import() inside the action, so pages where nobody clicks never download it. wp-scripts records it as a dynamic module dependency. |
| Instant-feeling clicks | data-wp-on--mouseenter and data-wp-on--focus call actions.prefetch to warm the router cache. |
| Loading state | data-wp-class--is-loading and data-wp-bind--aria-busy on the block wrapper, driven by state.isLoading. |
Correct preventDefault() in an async action |
The navigate action is wrapped in withSyncEvent() (WordPress 6.8+), which marks actions that need synchronous event access. |
| One store, three blocks | The save button, the grid cards and the header counter all use the fieldnote-interactive namespace from a shared module, so state changes propagate everywhere with no custom events. |
| "Is this post saved?" | Each button gets its own data-wp-context ({ id, title, url }). A single derived getter, state.isSaved, reads that context, and data-wp-bind--aria-pressed reflects it. |
| Server-rendered first paint | wp_interactivity_state() provides defaults, including derived state closures for isSaved, hasItems and buttonLabel that PHP evaluates for the initial HTML before the matching JavaScript getters take over. |
| Translated strings in JavaScript | wp_interactivity_config() passes translated labels; the store reads them with getConfig(). |
| Reading list items | ` |