Kinetra
Documentation · Kinetra 1.3.0

Developers

How Kinetra works, every hook and filter, the payload, JavaScript events and the frontend source.

All guides

This is the technical reference for Kinetra 1.3.0: how a request becomes the animated site, every PHP hook, the data the frontend receives, the browser events, and how to build on it safely. It assumes you know WordPress themes and plugins. Site owners don’t need any of it; start with After installation instead.

How Kinetra works

Kinetra is a normal PHP theme that ships a prebuilt Next.js and React app. There is no Node.js on the server. Every request goes through WordPress as usual:

  1. WordPress runs its query and loads index.php, the theme’s only page template.
  2. kinetra_build_payload() turns the current query into a JSON payload (the page), and kinetra_site_data() builds the site data (menus, settings, labels).
  3. The theme prints plain, crawlable HTML of the page in #kinetra-ssr for search engines and no-JavaScript visitors, followed by both JSON objects in <script id="__KINETRA__" type="application/json">.
  4. The app starts, reads that JSON, renders the animated page in #kinetra-root and adds the class nx-ready to <html>.
  5. When a visitor clicks an internal link, the app fetches the same URL with ?_kinetra=1, which returns just the payload as JSON, then animates to the new page and updates the title, meta tags and history.

Because the page is always built by WordPress, every plugin that changes the query, content or SEO data keeps working. Plugins that need a real page load can ask for one (see kinetra_needs_full_load).

Files

PathWhat it does
kinetra/functions.phpDefines KINETRA_VERSION, KINETRA_DIR, KINETRA_URI and loads inc/.
inc/options.phpOption schema, presets, palettes, fonts; kinetra_opt().
inc/payload.phpThe payload, site data, menus and the ?_kinetra JSON endpoint.
inc/render.phpThe crawlable HTML and the app root.
inc/assets.phpApp files, CSS variables, fonts, publishing to uploads.
inc/compat.phpCache clearing, optimizer exclusions, full-load detection, breadcrumbs.
inc/elementor.php, inc/elementor-hf.phpElementor pages, Theme Builder, style kits, header and footer templates.
inc/woocommerce.phpProduct data, shop archives, cart drawer, native WooCommerce pages.
inc/i18n.phpFrontend labels, language switchers, WPML/Polylang strings.
inc/importer.php, inc/demo/Demo registry, packages and import.
inc/license.php, inc/plugins.php, inc/admin.php, inc/wizard.phpLicence, plugin installer, Kinetra admin screens, Setup Wizard.
kinetra/app/The built frontend. Don’t edit; rebuild from source.
kinetra-core/inc/Projects, page meta, section blocks, SEO, contact, newsletter, custom code, uploads, Elementor widgets.

Constants

ConstantWhere to set itEffect
KINETRA_DEV_ORIGINwp-config.phpAllows a frontend dev server (for example http://localhost:3000) to read the JSON endpoint.
KINETRA_DEMO_SERVERwp-config.phpURL of a folder of demo packages ({id}.json) to import from.
KINETRA_VERSION, KINETRA_DIR, KINETRA_URIRead onlyTheme version, path and URL.
KINETRA_CORE_VERSION, KINETRA_CORE_DIR, KINETRA_CORE_URIRead onlyKinetra Core version, path and URL. Check defined( 'KINETRA_CORE_VERSION' ) before using Core functions.

Where to put your code

  • A small plugin or a child theme’s functions.php for PHP. Both load before the parent theme, so filters are in place in time.
  • Kinetra → Custom Code for tracking tags and CSS. Snippets go in wp_head, wp_body_open or wp_footer with a priority and a condition (every page, front page only, logged-in users and so on).
  • Customize → Additional CSS for style tweaks.

A child theme only needs a style.css:

/*
Theme Name: Kinetra Child
Template: kinetra
*/

Don’t add page.php, single.php, archive.php or similar to a child theme. WordPress would use them instead of Kinetra’s index.php, and those pages would lose the animated site. comments.php can be overridden; its output is shown inside the app.

Settings

Every Customizer option is defined once in kinetra_option_schema(). The schema registers the Customizer control, sanitises the value, gives it a default, and (unless public is false) sends it to the frontend as site.settings.

FunctionReturns
kinetra_opt( $key )A setting’s value, typed and with the active preset and palette defaults applied. Text is translated with WPML or Polylang.
kinetra_default( $key )The default: the colour palette first, then the style preset, then the schema.
kinetra_style()The active preset key, such as kinetic or launch.
kinetra_public_settings()All public settings, plus copyright with {year} and {site} filled in.
kinetra_core_opt( $key )The same as kinetra_opt(), with fallbacks when the theme isn’t active (Kinetra Core).

Values are stored as theme mods named kinetra_{key}, so get_theme_mod( 'kinetra_accent' ) gives the saved value only. Use kinetra_opt() to get the value with defaults.

A schema entry has section, type, label and default, and optionally description, choices (select), min/max (range, number), public and core. Types are select, toggle, color, range, number, text, textarea, url, email, code and palette.

Sections: kinetra_style, kinetra_colors, kinetra_typography, kinetra_layout, kinetra_header, kinetra_motion, kinetra_portfolio, kinetra_blog, kinetra_shop, kinetra_footer, kinetra_social, kinetra_contact, kinetra_seo, kinetra_advanced.

Add your own option

add_filter( 'kinetra_option_schema', function ( $schema ) {
	$schema['promo_text'] = array(
		'section' => 'kinetra_header',
		'type'    => 'text',
		'label'   => 'Promo text',
		'default' => '',
	);
	return $schema;
} );

// Anywhere later:
$text = kinetra_opt( 'promo_text' );

The schema is built once per request and then cached. Add this filter when your file loads, not inside a later hook such as init.

Design tokens (CSS variables)

The settings are printed as CSS custom properties on :root. Use them in your own CSS so it follows the Customizer and the light/dark switch:

VariableFrom
--nx-accent, --nx-accent-textAccent and the text colour on it
--nx-bg, --nx-surface, --nx-text, --nx-muted, --nx-border, --nx-highlightThe light or dark palette, whichever is showing
--nx-font-heading, --nx-font-bodyFont stacks
--nx-base, --nx-heading-weight, --nx-tracking, --nx-scaleText size, heading weight, letter spacing, display scale
--nx-container, --nx-gutter, --nx-sectionContent width, side padding, space between sections
--nx-radius, --nx-btn-radiusCorner radius of cards and buttons
--nx-marqueeMarquee loop duration

The active colour scheme is on <html data-scheme="light|dark">. <body> has the classes kinetra and kinetra-style-{preset}.

.my-badge {
	background: var(--nx-accent);
	color: var(--nx-accent-text);
	border-radius: var(--nx-btn-radius);
	font-family: var(--nx-font-heading);
}
:root[data-scheme="dark"] .my-badge { box-shadow: 0 0 0 1px var(--nx-border); }

Presets, palettes and fonts

// Change a preset's defaults.
add_filter( 'kinetra_presets', function ( $presets ) {
	$presets['launch']['defaults']['accent'] = '#0EA5E9';
	return $presets;
} );

// Add a colour combination to Customize → Colors.
add_filter( 'kinetra_palettes', function ( $palettes ) {
	$palettes['brand'] = array(
		'label'  => 'Our brand',
		'colors' => array( 'accent' => '#0B5FFF', 'accent_text' => '#FFFFFF', 'light_highlight' => '#0B5FFF', 'dark_highlight' => '#8DB2FF' ),
	);
	return $palettes;
} );

// Add a Google Font to the font choices.
add_filter( 'kinetra_fonts', function ( $fonts ) {
	$fonts['Lexend'] = array( 'spec' => 'wght@400;500;600;700', 'fallback' => 'sans-serif' );
	return $fonts;
} );

A new preset key works too. It sets option defaults and uses the Kinetic section layouts; the six built-in layouts are part of the frontend app.

Content and sections

A page is turned into a list of sections. Each kinetra/* block becomes { type: "block", name, data }. Everything between them (paragraphs, images, embeds, shortcodes, other plugins’ blocks) is rendered by WordPress and grouped into { type: "html", html }. Classic-editor content becomes one HTML section.

Each section is defined once in kinetra_block_schemas() in Kinetra Core. The schema registers the block (kinetra/{name}), builds its editor form, registers the Elementor widget (kinetra-{name}), renders the crawlable HTML, and shapes the data the app gets.

'hero' => array(
	'title'       => 'Hero',
	'description' => 'Big animated headline with intro text and buttons.',
	'icon'        => 'superhero', // Dashicon name
	'fields'      => array(
		'eyebrow' => array( 'type' => 'text', 'label' => 'Small label', 'default' => '' ),
		'image'   => array( 'type' => 'image', 'label' => 'Side image', 'default' => 0 ),
		// …
	),
),

Field types: text, textarea, url, color, number (min, max), toggle, select (choices), image (attachment ID; the app gets url, width, height, alt, srcset) and list (a repeater with its own fields).

Section names: hero, marquee, projects, feature, services, tabs, showreel, about, process, quote, clients, logos, testimonials, team, stats, features, pricing, faq, cta, newsletter, contact, products, categories, promo, banners, perks.

Change section defaults or fields

add_filter( 'kinetra_block_schemas', function ( $schemas ) {
	// New Hero blocks start with your own button text.
	$schemas['hero']['fields']['primary_label']['default'] = 'Book a call';

	// Give the Call to Action a field your own code can read.
	$schemas['cta']['fields']['campaign'] = array( 'type' => 'text', 'label' => 'Campaign ID', 'default' => '' );
	return $schemas;
} );

The built app only draws the 26 sections it knows, and only the fields each one was written for. A new field reaches the payload (and the kinetra_payload filter) straight away; to show it, or to add a completely new section, change the frontend source and rebuild. A new section name without a frontend component renders nothing on the animated site.

Replace a page’s sections

kinetra_pre_content_sections runs before the content is parsed. Return an array of sections to skip parsing, or null to continue.

add_filter( 'kinetra_pre_content_sections', function ( $sections, $post ) {
	if ( 'landing' !== $post->post_name ) {
		return $sections; // null: parse as usual
	}
	return array(
		array( 'type' => 'block', 'name' => 'hero', 'data' => kinetra_block_data( 'hero', array( 'title' => "Hello\nworld" ) ) ),
		array( 'type' => 'html', 'html' => '<p>Rendered by my plugin.</p>' ),
	);
}, 10, 2 );

Post types, fields and meta

ItemDetails
ProjectsPost type kinetra_project, taxonomy kinetra_project_cat. The URL slug is the portfolio_slug option (default work); categories use {slug}-category. Supports title, editor, thumbnail, excerpt, page attributes, revisions and custom fields. In the REST API.
Project meta_kinetra_client, _kinetra_role, _kinetra_year, _kinetra_live_url, _kinetra_art_bg, _kinetra_art_fg, _kinetra_featured
Page and post meta_kinetra_hide_title, _kinetra_seo_title, _kinetra_seo_description, _kinetra_noindex. Registered with show_in_rest.
Demo markers_kinetra_demo on everything an import created. Remove demo content deletes items with this meta.
Elementor templates_kinetra_hf (header or footer) and _kinetra_hf_hash on the templates Kinetra creates.
Menusprimary (Main menu, with drop-downs) and footer.
Widget areasfooter-widgets and the shop sidebar.
Image sizeskinetra-card (1040 × 880, cropped) and kinetra-wide (1920 × 1080).
Shortcode[kinetra_year] prints the current year.
Optionskinetra_subscribers (newsletter list), kinetra_code_snippets (Custom Code).

Useful functions: kinetra_get_projects( array( 'count' => 4, 'category' => 'branding', 'featured' => true ) ), kinetra_project_card( $post ), kinetra_post_card( $post ), kinetra_image( $id, $size ), kinetra_menu( $location ).

The payload

The payload is what the app draws for one URL. Change it with kinetra_payload. It always has kind, seo (title, description, canonical, image, noindex), url and lang.

kindMain keys
singlepostType, id, title, excerpt, sections, image, date, dateLabel, author, readingTime, hideTitle, isFront, terms, tags, classes, pageLinks, comments, breadcrumbs, native, reload. Posts add adjacent; projects add project and nextProject; products add product.
archivearchiveType (blog, projects, products, search, archive), eyebrow, title, description, items (cards), filters, activeFilter, pagination, query, shop.
404title, text.

With Elementor, builder holds the page’s Elementor content and Theme Builder header and footer, plus layout (canvas or full).

add_filter( 'kinetra_payload', function ( $payload ) {
	if ( 'single' === $payload['kind'] && 'post' === $payload['postType'] ) {
		$payload['readingTime'] = max( 1, $payload['readingTime'] - 1 );
		$payload['sponsor']     = get_post_meta( $payload['id'], 'sponsor', true );
	}
	return $payload;
} );

To see the payload of any page, open it with ?_kinetra=1 added to the address. Use ?_kinetra=boot to get the site data as well.

Full page loads

Some content needs a real page load: its scripts only start on DOMContentLoaded, or it prints its own assets. Kinetra already does this for pages with Contact Form 7, WPForms, Gravity Forms, Fluent Forms, Ninja Forms, Formidable, Mailchimp for WP, Everest Forms and Forminator, and for WooCommerce cart, checkout and account pages. Add your own with kinetra_needs_full_load:

add_filter( 'kinetra_needs_full_load', function ( $needs, $post ) {
	return $needs || has_shortcode( $post->post_content, 'my_booking_widget' );
}, 10, 2 );

In your own HTML, add data-native to a link to open it with a normal page load. Links with a target other than _self, with download, to files, to wp-admin, wp-json, feeds and to other sites always load normally.

JavaScript in the browser

Page changes don’t reload the page, so scripts that set themselves up on page load run only once. Use these events to run your code again:

EventFired onWhendetail
kinetra:navigatewindowAfter the app has moved to a new page and updated the address and title.{ url, title }
kinetra:contentdocumentEach time WordPress HTML (normal blocks, shortcodes, embeds) is put on the page, including the first load.{ root }, the element that holds it

Kinetra also triggers jQuery’s post-load event on document.body (used by Jetpack and many lazy loaders) and starts Contact Form 7, WPForms and Gravity Forms in new content. The class nx-ready appears on <html> once the app has started.

// Run a widget library on new content.
document.addEventListener('kinetra:content', function (e) {
  e.detail.root.querySelectorAll('.my-slider').forEach(function (el) {
    MySlider.init(el);
  });
});

// Send a page view to a tool that doesn't track history changes.
window.addEventListener('kinetra:navigate', function (e) {
  if (window.plausible) plausible('pageview', { u: e.detail.url });
});

// Wait for the app before measuring layout.
function whenReady(fn) {
  var html = document.documentElement;
  if (html.classList.contains('nx-ready')) return fn();
  new MutationObserver(function (m, o) {
    if (html.classList.contains('nx-ready')) { o.disconnect(); fn(); }
  }).observe(html, { attributes: true, attributeFilter: ['class'] });
}

Google Analytics 4 (Kinetra’s own setting or Site Kit) needs nothing extra: the title is updated before the history change, and GA4 counts history changes as page views by default.

Snippets in Kinetra → Custom Code are printed on the first page load only. A snippet that must run on every page should listen for kinetra:navigate.

Hooks reference

Every filter and action Kinetra provides. Theme hooks run when the Kinetra theme is active; Kinetra Core hooks run when the plugin is active.

Design and settings

HookTypeArgumentsUse it to
kinetra_option_schemaFilter$schemaAdd, change or remove Customizer options.
kinetra_presetsFilter$presetsAdd design presets or change their defaults.
kinetra_palettesFilter$palettesAdd colour combinations.
kinetra_fontsFilter$fontsAdd Google Fonts (spec is the css2 axis string).
kinetra_i18nFilter$labelsChange the frontend’s button and screen-reader labels, such as menu or skip.
kinetra_languagesFilter$languagesChange the language switcher items (code, name, url, current).

Pages and content

HookTypeArgumentsUse it to
kinetra_payloadFilter$payloadChange or add to the data sent to the app for a page.
kinetra_pre_content_sectionsFilternull, $postSupply a page’s sections yourself.
kinetra_block_schemasFilter$schemasChange section fields and defaults (blocks and Elementor widgets). Core.
kinetra_needs_full_loadFilter$needs, $postMake a page open with a normal page load.
kinetra_wc_native_pageFilter$nativeLet WooCommerce render the current page itself (default: cart, checkout, account; with classic templates also shop and products).

SEO

HookTypeArgumentsUse it to
kinetra_seo_dataFilter$seoChange the title, description, canonical, image or noindex for the current page. Used for the head tags and for in-app navigation. Core.
kinetra_schema_graphFilter$graphAdd or change JSON-LD nodes (WebSite, Person or Organization, BreadcrumbList…). Core.
kinetra_faq_schemaFiltertrueReturn false to stop FAQPage data for FAQ sections. Core.
add_filter( 'kinetra_schema_graph', function ( $graph ) {
	$graph[] = array(
		'@type'     => 'LocalBusiness',
		'name'      => get_bloginfo( 'name' ),
		'telephone' => '+44 20 0000 0000',
	);
	return $graph;
} );

Forms and email

HookTypeArgumentsUse it to
kinetra_contact_submittedAction$fields (name, email, message, type), $sentSave messages to a CRM, Slack, a help desk… Runs after the email is attempted. Core.
kinetra_newsletter_signupAction$emailSend signups to Mailchimp, Brevo, ConvertKit and so on. Core.
kinetra_store_subscribersFiltertrueReturn false to stop saving signups in Kinetra → Subscribers. Core.
add_action( 'kinetra_newsletter_signup', function ( $email ) {
	wp_remote_post( 'https://api.example-mail.com/v1/subscribers', array(
		'headers' => array( 'Authorization' => 'Bearer ' . MY_MAIL_API_KEY ),
		'body'    => array( 'email' => $email ),
	) );
} );
add_filter( 'kinetra_store_subscribers', '__return_false' );

Speed, caching and scripts

HookTypeArgumentsUse it to
kinetra_purge_cachesActionnoneClear your own cache when Kinetra clears caches (Customizer save, demo import, custom code save, theme switch).
kinetra_settings_changedActionnoneRuns when Kinetra settings outside the Customizer change, such as Custom Code. Core.
kinetra_force_asset_proxyFilterfalseReturn true to serve the app’s files through PHP instead of copying them to uploads (for read-only file systems).
kinetra_disable_custom_codeFilterfalseReturn true to stop Custom Code snippets, for example on a page you are debugging. Core.
kinetra_core_cleanup_headFiltertrueReturn false to keep the emoji script, generator tag and other legacy head links. Core.
kinetra_consent_tool_activeFilterboolTell Kinetra a cookie-consent tool that blocks scripts is active (Complianz is detected automatically). Core.
kinetra_dev_originFilter$originThe origin allowed to read the JSON endpoint during frontend development.
add_action( 'kinetra_purge_caches', function () {
	if ( function_exists( 'my_cdn_purge_all' ) ) {
		my_cdn_purge_all();
	}
} );

Uploads

HookTypeArgumentsUse it to
kinetra_svg_capabilityFilter'edit_others_posts'The capability needed to upload SVG files. Core.
kinetra_svg_elementsFilter$elementsThe SVG elements kept when an upload is cleaned. Core.

Demos, plugins and setup

HookTypeArgumentsUse it to
kinetra_demosFilter$demosAdd a demo to the Setup Wizard (label, tagline, description, preview, optional plugins).
kinetra_demo_packageFilternull, $idReturn the content of your demo as an array.
kinetra_demo_serverFilter$urlLoad demo packages from {url}/{id}.json.
kinetra_demo_importedAction$demoRun your own setup after an import.
kinetra_pluginsFilter$pluginsChange the Kinetra → Plugins list (name, file, source of repo or bundled, package, required, group, description).
kinetra_core_packageFilter$pathWhere the bundled kinetra-core.zip is.

Add your own demo

Agencies can ship their own starter site in the Setup Wizard. Register it, then return its package:

add_filter( 'kinetra_demos', function ( $demos ) {
	$demos['studio'] = array(
		'label'       => 'Our studio starter',
		'tagline'     => 'Agency · clean',
		'description' => 'Our standard client site.',
		'preview'     => plugins_url( 'preview.svg', __FILE__ ),
	);
	return $demos;
} );

add_filter( 'kinetra_demo_package', function ( $package, $id ) {
	if ( 'studio' !== $id ) {
		return $package;
	}
	return array(
		'id'       => 'studio',
		'site'     => array( 'name' => 'Studio', 'tagline' => 'Design and build' ),
		'settings' => array( 'style' => 'collective', 'accent' => '#FF5A1F' ),
		'pages'    => array(
			array(
				'title'   => 'Home',
				'slug'    => 'home',
				'role'    => 'front',
				'meta'    => array( 'hide_title' => '1' ),
				'content' => '<!-- wp:kinetra/hero {"title":"We make\\nthings move."} /-->',
			),
		),
		'projects' => array(),
		'posts'    => array(),
		'menus'    => array( 'primary' => array( array( 'title' => 'Home', 'page' => 'home' ) ) ),
	);
}, 10, 2 );

Page role can be front (set as the home page) or blog (set as the posts page). settings takes option keys from the schema. Look at kinetra/inc/demo/*.php for full examples with projects, posts, products and images.

REST API

EndpointBodyNotes
POST /wp-json/kinetra/v1/contactname, email (required), message, type, consentSends the email to the Contact Form recipient. Five messages per IP per hour. Returns { ok, message }.
POST /wp-json/kinetra/v1/subscribeemail (required)Saves the signup and fires kinetra_newsletter_signup. Five per IP per hour.
GET /any-page/?_kinetra=1noneThe payload of that page as JSON. Sent with X-Robots-Tag: noindex and a 60-second public cache for visitors.

Both POST endpoints have a hidden website field as a spam trap: if it is filled in, the request looks successful but nothing is sent.

Elementor

  • Each section is an Elementor widget named kinetra-{section} in the Kinetra Sections category, built from the same schema as the block.
  • Elementor pages, and Elementor Pro Theme Builder single, archive, 404, header and footer templates, are rendered by Elementor and placed inside the app.
  • The Theme Builder header sits in .nx-builder-header and the footer in .nx-builder-footer. Kinetra adds kx-sticky, kx-scrolled and kx-hidden to the header from the Header & Menu settings, so style against those instead of using Elementor Pro’s sticky option.
  • When Elementor Pro (or Pro Elements) is active, Kinetra creates “Kinetra Header” and “Kinetra Footer” templates from the Customizer settings and keeps them updated until you edit them in Elementor. They show on the site once their Display Conditions are set to Entire Site.
  • One Elementor kit per preset (“Kinetra — Kinetic” and so on) mirrors the Customizer’s colours and fonts as Elementor globals.

WooCommerce

  • Shop, category and product pages are drawn by the app by default. With Customize → Shop → WooCommerce classic templates, or for cart, checkout and account, WooCommerce renders the page and the app places it in its layout (#kinetra-native). All WooCommerce hooks and template overrides work on those pages.
  • On animated product pages, the product data comes from kinetra_product_payload(). Change it in kinetra_payload under product.
  • woocommerce_short_description and woocommerce_default_catalog_orderby are respected.
  • Links with add-to-cart, remove_item, wc-ajax or customer-logout in the address always load normally.

Translation

  • Text domains are kinetra (theme) and kinetra-core (plugin), with .pot files in each languages folder.
  • Customizer text options are registered as WPML strings and Polylang strings in the Kinetra context.
  • The frontend’s labels come from kinetra_i18n(), so they are translated with the theme.
  • When a visitor switches language, the app does a full page load.

Changing the frontend

The Professional and Agency plans include kinetra-frontend-source.zip, the Next.js source (React 19, Framer Motion, Lenis). Work against a local WordPress site:

cd frontend
npm install
echo "NEXT_PUBLIC_WP_URL=http://your-local-wp.test" > .env.local
npm run dev

Allow the dev server in that site’s wp-config.php:

define( 'KINETRA_DEV_ORIGIN', 'http://localhost:3000' );
FolderWhat is in it
src/components/App shell, routing (App.tsx), header, footer, page transitions, effects, Elementor integration.
src/sections/One component per section. index.tsx maps section names to components in BLOCKS.
src/views/Single, archive, search, 404 and shop views.
src/lib/Payload types (types.ts), fetching (api.ts), motion presets, cart, labels.
app/globals.cssAll styles, built on the --nx-* variables.

To add a section: add its schema with kinetra_block_schemas, write a component that takes { data }, add it to BLOCKS under the same name, then build. npm run build exports the app and copies it into kinetra/app/; Kinetra publishes the new build to uploads on the next visit.

A theme update replaces kinetra/app/ with the official build. If you ship a changed frontend, put it in a child theme’s own copy of the theme, or rebuild after every update, and keep your changes in version control.

Debugging checklist

  • Kinetra → Dashboard → System status shows the app build, uploads folder, Kinetra Core and SEO system.
  • Open the page with ?_kinetra=1 to see exactly what the app receives.
  • View the page source: the plain HTML in #kinetra-ssr is what search engines read.
  • If the page shows plain text for a few seconds and then the plain version, the app didn’t start. Check the browser console and optimizer plugins first.
  • Turn off Custom Code with add_filter( 'kinetra_disable_custom_code', '__return_true' ); to rule it out.
  • Clear caches after changing PHP filters; payloads are cached for 60 seconds for visitors.