Skip to content

Single-Page Apps

Single-page apps let you version-control a front-end application (Vue, React, Svelte, anything that builds to static files) alongside the rest of your WordPress config, ship its built output to the site over the REST API, and drop it into a page with a shortcode.

Loopress ships the build output. When the app has a package.json with a build script, lps app push runs it first with the app’s own package manager (read from packageManager, else from the nearest lockfile, else npm), so a forgotten rebuild never ships a stale dist/. Without a build script, the dist/ folder is pushed as is. Loopress never installs dependencies: run the install yourself once.

apps/
search/
loopress.app.json # a few optional settings
dist/ # your build output, this is what gets pushed
index.html
assets/
index-9a597e0d.js
index-51910369.css
Terminal window
# 1. install the app's dependencies, once
npm --prefix apps/search install
# 2. build and ship the bundle to WordPress (only changed files are uploaded)
lps app push search
# 3. mount it in a page
[loopress_app name="search"]

The shortcode enqueues the app’s content-hashed entry files and prints the mount point the SPA attaches to:

<div id="loopress-app-search" data-loopress-app="search"></div>

Every field is optional.

Field Default Notes
name the directory name lowercase letters, digits and hyphens
assetsDir dist build output directory, relative to the app directory
mountSelector #loopress-app-<name> the element id the shortcode renders and your app mounts on
routing hash only hash is supported (see below)
entry read from dist/index.html { "scripts": [...], "styles": [...] }, only needed when there is no index.html
crossOrigin unset "same-origin" or "same-site": stops other sites from embedding the app’s files (see below)

Keeping other sites from embedding your app

Section titled “Keeping other sites from embedding your app”

By default, any website can load your app’s files straight from your server with <script src="https://your-site/wp-content/loopress/apps/<name>/...">. Set crossOrigin to stop that:

{ "crossOrigin": "same-origin" }

On the next lps app push, the plugin writes an .htaccess in the app’s folder that sends Cross-Origin-Resource-Policy: same-origin (or same-site) with every file, and browsers then refuse to load them from another site. Remove the setting and push again to lift it.

  • same-origin allows only your site’s exact domain. same-site also allows its subdomains, for example cdn.your-site.com.
  • CDN on another domain. If a CDN rewrites your asset URLs to a different domain (for example *.b-cdn.net), browsers will refuse the app’s CSS and images too. Leave crossOrigin unset in that case.
  • It does not hide the code. Anything a browser runs can be downloaded and copied. The setting only stops other sites from using your server to run it.
  • Apache and LiteSpeed only. nginx ignores .htaccess: add the header for wp-content/loopress/apps/<name>/ in the server config instead.
  • Plugin version. An older Loopress plugin commits the build but ignores the setting; lps app push warns you when that happens.
  • Your own rules win. If apps/<name>/.htaccess on the server does not start with # Loopress:, Loopress leaves it untouched and lps app push warns that the setting was not applied.
  • Hash routing only. vue-router / React Router must run in hash mode (/page#/route). History-mode routing needs a server rewrite rule that would collide with WordPress permalinks and other plugins, so it is refused for now rather than half-supported.
  • Content-hashed filenames. Vite and webpack do this by default (index-9a597e0d.js). It is what makes a new deploy bust every cache; the enqueue version is also set to the build id.
  • Static assets only. .php and other server-executable files are rejected on push: the bundle is served straight off wp-content/.
  • Per-file size limit. A single file over 8 MB is rejected (raise it with the loopress_app_max_asset_bytes filter). A typical search-page bundle is 1 to 3 MB total and well under this.
  • One generation of grace. On deploy, files from the immediately previous build are kept so a visitor mid-session does not 404 on a lazy chunk. Older builds are removed. There is no rollback or version history.
  • One deploy at a time per app. Concurrent lps app push (or a push racing a lps app remove) for the same app are not serialized: the asset cleanup and the state write are separate steps. Deploys of different apps are independent. Run one push per app at a time, which is the normal case for a CI pipeline.
  • Not part of lps push. Apps have their own build step, so the aggregate lps push / lps pull / lps promote commands leave them alone. Deploy with lps app push.

The shortcode exposes a small config object on the page, named loopressApp_<name> (hyphens become underscores):

// window.loopressApp_search
{
name: 'search',
mount: '#loopress-app-search',
base: 'https://example.com/wp-content/loopress/apps/search/',
buildId: '9f2a1c7b4e10',
restUrl: 'https://example.com/wp-json/',
restNonce: 'a1b2c3d4e5',
}

base is the absolute URL the bundle is served from: use it to resolve a public/ asset referenced from rendered markup (base + 'icon.svg') that the bundler does not rewrite. Pair restUrl / restNonce with a custom API route for the search endpoint itself.

  • CLI: the lps app pull/push/list/remove commands