A WordPress theme that makes your site feel like a modern web app. Pages load instantly without full reloads, while keeping everything SEO-friendly and server-rendered.
Built with the WordPress Interactivity API and a custom SPA (Single Page Application) router.
Quick Start | Features | How It Works | Roadmap | File Structure | Contributing
- Instant navigation — SPA-like routing with server-rendered HTML
- Interactivity API — WordPress 6.5+ partial hydration for interactive blocks
- 4 custom blocks — Navigation (SPA router + responsive menu), Search (live REST API search), Counter, Accordion
- WooCommerce compatible — Full theme support, product gallery, blocks, shop sidebar; cart/checkout/my-account use full page load; fixes Order Attribution
CustomElementRegistryconflict - Design tokens — CSS custom properties for easy theming (see THEME-CUSTOMIZATION.md)
- Print styles — Clean print output with navigation/footer hidden
- TypeScript — All block source code is typed
The theme combines SPA-style instant navigation with full server-side rendering. You get the perceived speed of a client-side app without sacrificing SEO or the reliability of PHP templates. Content is always server-rendered; the SPA layer only swaps HTML fragments on navigation.
- WordPress 6.5+ (or Gutenberg 17.5+)
- PHP 7.4+
- Node.js 18+ (for building)
# 1. Clone into your themes folder
cd wp-content/themes/
# 2. Install dependencies
cd interactivity-theme
npm install
# 3. Build block assets
npm run build
# 4. Activate in WordPress Admin > Appearance > ThemesFor development with live rebuilds:
npm run devEvery link click triggers a full page reload:
- Browser requests a new HTML document
- Re-downloads and re-parses CSS, JS, fonts, images
- Re-renders the entire page (header, footer, sidebar, content)
- Typical navigation: 800ms - 2s+ per page
Link clicks only fetch the content that changed:
- Browser fetches a small JSON response with just the new content HTML
- Header, footer, sidebar, and styles stay in place
- Only the content area swaps out
- Typical navigation: 50ms - 200ms per page
| Metric | Traditional WP | Interactivity Theme |
|---|---|---|
| Data per navigation | Full HTML document (~100-500 KB) | Content fragment (~5-50 KB) |
| CSS/JS re-parse | Every page load | Once (initial load only) |
| DOM re-render | Entire page | Content area only |
| Perceived load time | 800ms - 2s+ | 50ms - 200ms |
| Cached pages | None (browser cache only) | localStorage + server transients |
| SEO | Full SSR | Full SSR (identical) |
-
Client-side routing — Internal links are intercepted before the browser navigates. A lightweight fetch grabs server-rendered HTML from
/theme/route-htmland swaps it into the page. -
Two-layer caching — Archive and list pages are cached in
localStoragefor 5 minutes (up to 50 routes). The server also caches REST responses with WordPress transients. Single posts always fetch fresh so comments stay current. -
Minimal JavaScript — The SPA router is ~15 KB. Interactive blocks (accordion, search, counter) use the Interactivity API for partial hydration — only the parts that need interactivity get JavaScript. Cart, checkout, and my-account use full page loads for reliability.
-
Dynamic asset loading — When navigating to a post that uses special blocks or plugins, only the CSS/JS for that specific post is loaded on demand. No upfront bundle of every possible asset.
-
Server rendering — All content comes from PHP templates. No client-side template building. Google sees the same HTML as your visitors.
flowchart TD
A[User clicks link] --> B[Inline script intercepts click]
B --> C[Dispatches theme:spa:navigate event]
C --> D[Navigation router view.ts picks it up]
D --> E{Is it cached?}
E -- YES --> F[Use cached HTML]
E -- NO --> G[Fetch /wp/v2/theme/route-html]
G --> H[Server renders HTML using PHP templates]
H --> F
F --> I[Swap content into page]
I --> J[Update title, body classes, browser history]
J --> K[Load post-specific CSS/JS on demand]
| Endpoint | What it does |
|---|---|
GET /wp/v2/theme/route-html?path=... |
Returns server-rendered HTML for any route |
GET /wp/v2/theme/global-styles |
Block editor + Customizer CSS for SPA injection |
GET /wp/v2/theme/post-assets?post_id=... |
CSS/JS handles needed for a specific post |
GET /wp/v2/theme/page-by-path?path=... |
Hierarchical page lookup by URL path |
GET /wp/v2/cached/posts |
Posts with server-side transient caching (5 min) |
GET /wp/v2/cached/pages |
Pages with server-side transient caching (5 min) |
| URL pattern | Template | Content partial |
|---|---|---|
| Front page | front-page.php |
Handles both "latest posts" and "static page" |
| Single post | single.php |
template-parts/route-single.php |
| Page | page.php |
template-parts/route-page.php |
| Archive / Home / Search | index.php, archive.php, search.php |
template-parts/route-loop.php |
| Shop / Product / Product archive | woocommerce.php |
WooCommerce content; cart/checkout/my-account use page.php (full page load) |
Planned features and improvements, prioritized by impact:
| Priority | Category | Items |
|---|---|---|
| P1 (Critical) | Accessibility & Fixes | Skip-to-content, focus trap in search overlay, aria-live for SPA content, prefers-reduced-motion, fix search block postType/maxResults |
| P2 (Features) | New Features | Breadcrumbs, Open Graph/Twitter Cards, JSON-LD, dark mode, related posts, TOC block, mini-cart, infinite scroll |
| P3 (Improvements) | UX & Performance | Responsive breakpoints, SPA error handling, loading skeleton, responsive images, lazy loading, cache invalidation, dynamic WooCommerce paths |
| P4 (Code Quality) | Dev Experience | Unit tests, E2E tests, constants extraction, i18n, navigation block editor, service worker |
Implementation follows phased rollout: Phase 1 (P1) → Phase 2 (P2) → Phase 3 (P3) → Phase 4 (P4).
Instant feel without sacrificing SEO. Content is always server-rendered; the SPA layer only swaps HTML fragments on navigation. Search engines and users without JavaScript see the same full HTML.
Reliability and compatibility with WooCommerce flows. Cart, checkout, and my-account use full page loads to avoid conflicts with payment gateways, session handling, and order attribution scripts.
Two-layer caching: archives and list pages are cached in localStorage (5 min, up to 50 routes) for fast repeat visits; the server caches REST responses with transients. Single posts always fetch fresh so comments stay current.
interactivity-theme/
├── style.css # Theme metadata + design tokens
├── print.css # Print-friendly styles
├── functions.php # REST routes, caching, WooCommerce support, SPA setup
├── front-page.php # Front page (latest posts or static page)
├── index.php / archive.php / search.php / single.php / page.php
├── woocommerce.php # Shop, product archives, single product
├── header.php / footer.php / sidebar.php / sidebar-shop.php / comments.php
├── template-parts/
│ ├── route-single.php # Single post content (used by SPA + direct load)
│ ├── route-page.php # Page content
│ └── route-loop.php # Archive / home / search loop
├── blocks/ # Block source (TypeScript + PHP)
│ ├── navigation/ # SPA router + responsive menu
│ │ ├── view.ts # Routing, caching, asset injection, SPA skip for WooCommerce
│ │ └── render.php # Server-rendered menu markup
│ ├── search/ # Live search with REST API
│ ├── counter/ # Increment/decrement demo
│ └── accordion/ # Collapsible sections
├── build/blocks/ # Compiled output (generated)
├── types/wordpress.d.ts # TypeScript declarations for wp global
├── tsconfig.json
├── webpack.config.js
├── package.json
├── phpcs.xml.dist # WordPress Coding Standards config
├── THEME-CUSTOMIZATION.md # Design token reference
└── README.md
npm run lint # Run all linters
npm run lint:php # PHP (WordPress Coding Standards via PHPCS)
npm run lint:php:fix # Auto-fix PHP issues
npm run lint:js # TypeScript/JavaScript (ESLint)
npm run lint:js:fix # Auto-fix JS issues
npm run lint:css # CSS (stylelint)PHP linting requires Composer: composer install
Override CSS design tokens in a child theme or the Customizer's Additional CSS. Full token reference: THEME-CUSTOMIZATION.md
| Problem | Fix |
|---|---|
| Blank content on refresh | Make sure template-parts/route-page.php, route-single.php, route-loop.php exist |
| Stale content after SPA nav | Clear localStorage key theme_route_cache |
WooCommerce CustomElementRegistry error |
Theme auto-dequeues order-attribution script on non-checkout pages -- check functions.php is active |
wp-interactivity not registered |
Run npm run build; theme uses ES modules with @wordpress/interactivity dependency |
| Blocks not in editor | Run npm install && npm run build, verify WP 6.5+ |
- Star this repo
- Fork & create a branch
- Follow existing code style (PHPCS, ESLint)
- Test with WordPress 6.5+ and WooCommerce
- Submit a PR
Aris — madebyaris.com
- GitHub: @madebyaris
- X: @arisberikut
- LinkedIn: arissetia
GNU General Public License v2 or later
Made with care by Aris Setiawan