Projects in Code, Craft, and Curiosity

GrimShadow

Running a Fullscreen JS App On WordPress

Getting a Fullscreen JS App running on a WordPress site was trickier than expected. Here is how I did it. I migrated the 2D G-Code Viewer from a separate subdomain to a route on the main site. I did it to consolidate SEO and branding, simplify infrastructure, and make future development easier. The move exposed interactions between theme CSS, canvas sizing, and caching. I turned off server and CDN caches to debug, but browser caching still served older JS until I versioned assets

New home: grimshadow.net/gcodeviewer
Old home:
g-codeviewer.grimshadow.net
Original post: /programs/2d-g-code-viewer/


Why I moved it off the subdomain

One site, not two

I originally put the viewer on a subdomain because it started as a standalone single-file site. That worked fine for a quick share, but the minute I wanted proper SEO, Google Analytics, and consistent privacy and consent policies it doubled the work. As a hobby project, I do not want to maintain two separate sites. Moving it to grimshadow.net/gcodeviewer lets the viewer benefit from the main domain’s authority, internal links, and crawl budget, and it makes updates much faster.

Shared theme and brand

Header and footer are now consistent across the domain. I can use plugins to manage analytics and other sitewide pieces in one place, which also makes monitoring simpler.

Simpler deployment and analytics

One SSL chain, one backup target, one cache policy, one analytics property. Any cross-origin weirdness went away the moment it lived under the main host.

Stronger internal linking

Blog posts, docs, and the viewer now live under one roof. It is easier for readers to bounce between explanations and the tool itself.


How and why I used a child theme to run a fullscreen JS app inside WordPress

There are two parts to getting a fullscreen canvas app to behave inside WordPress:

  1. protect your work from theme updates
  2. escape the normal article chrome so your canvas can fill the viewport and measure correctly

Editing a parent theme is fragile. One update and your edits are gone. A child theme gives you a safe place to add a blank page template, enqueue only the scripts you need, and override styles without touching the parent.

The moving pieces

  • Child theme: holds your template file, a functions.php, and your viewer assets.
  • Blank page template: renders a bare page that still runs WordPress hooks, but hides the parent’s header, footer, and padding on that one page only.
  • Targeted enqueue: load PIXI and your app.js only when that template is used.
  • Viewport layout CSS: fixed wrapper that fills the screen, a flex row, and a plot panel that grows.
  • Accurate sizing: JS measures getBoundingClientRect() from the real plot box, resizes the PIXI renderer, then fits the world.

1) Create the child theme

/wp-content/themes/"your-theme"ChildTheme/style.css:
/*
 Theme Name: "your-theme"ChildTheme
 Template: your-parent-theme
 Version: 1.0.0
 Author: Orion Lang (Grimshadow)
 Author URI: https://grimshadow.net
 Description: Child theme with print styling and layout tweaks
 License: GNU General Public License v3 or later
 License URI: https://www.gnu.org/licenses/gpl.html


mostly contains irrelevant details from my main theme edits
*/

The Template: line must match your parent folder name exactly.

/wp-content/themes/"your-theme"ChildTheme/functions.php:
<?php
/**
 * Functions for "your-theme"ChildTheme
 * Keep this file minimal and safe across parent updates.
 */

/**
 * 1) Enqueue parent then child stylesheet
 * This ensures parent CSS loads first, then your overrides.
 */
add_action('wp_enqueue_scripts', function () {
  $parent = wp_get_theme(get_template());
  wp_enqueue_style(
    'parent-style',
    get_template_directory_uri() . '/style.css',
    [],
    $parent ? $parent->get('Version') : null
  );

  wp_enqueue_style(
    'child-style',
    get_stylesheet_uri(),
    ['parent-style'],
    wp_get_theme()->get('Version')
  );
});

/**
 * 2) Register Header Left Widget area
 * Moved from parent into child so it survives updates.
 *
 * Guarded to avoid redeclare and duplicate sidebar IDs.
 */
if (!function_exists('grimshadow_register_header_widget')) {
  function grimshadow_register_header_widget() {
    global $wp_registered_sidebars;

    // If parent already registered this exact ID, do nothing
    if (isset($wp_registered_sidebars['header-left-widget'])) {
      return;
    }

    register_sidebar([
      'name'          => __('Header Left Widget', '"your-theme"ChildTheme'),
      'id'            => 'header-left-widget',
      'before_widget' => '<div id="%1$s" class="widget %2$s header-widget">',
      'after_widget'  => '</div>',
      'before_title'  => '<h3 class="widget-title">',
      'after_title'   => '</h3>',
    ]);
  }
  // Run after parent widgets_init so our guard can see any existing sidebars
  add_action('widgets_init', 'grimshadow_register_header_widget', 11);
}

/**
 * 3) Enqueue G-Code Viewer assets only on the blank template
 * Template file: page-gcode-viewer-blank.php in this child theme.
 */
// Load Pixi + your viewer files ONLY on the blank template, after WP knows the template
add_action('wp', function () {
  if (is_page_template('page-gcode-viewer-blank.php')) {
    add_action('wp_enqueue_scripts', function () {
      wp_enqueue_script('pixi', 'https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.2.4/pixi.min.js', [], '7.2.4', true);
      wp_enqueue_style('gcv-css', get_stylesheet_directory_uri() . '/gcv/app.css', [], '1.0');
      wp_enqueue_script('gcv-js',  get_stylesheet_directory_uri() . '/gcv/app.js', ['pixi'], '1.0', true);
   

    }, 20);
  }
});
	  

/**
 * Note:
 * - Do not close PHP with "?>" at the end of this file to avoid accidental whitespace output.
 */

2) Add a blank page template

Using a template-specific body class lets me hide the theme chrome only on this one page, so I do not have to fork the parent header or footer. Everything else on the site stays normal.

/wp-content/themes/"your-theme"ChildTheme/page-gcode-viewer-blank.php:
<?php /* Template Name: G-Code Viewer (Blank) */ ?>
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width">
<?php wp_head(); ?>
<style>
  html,body,#app-root { height: 100%; }
  html,body { margin: 0; background:#121212; }
  /* Hide theme chrome only on this template */
  body.gcv-blank .site-header,
  body.gcv-blank header[role="banner"],
  body.gcv-blank .site-footer,
  body.gcv-blank .sidebar,
  body.gcv-blank #wpadminbar { display:none !important; }
  body.gcv-blank .site, body.gcv-blank .entry-content { margin:0; padding:0; }
</style>
</head>
<body <?php body_class('gcv-blank'); ?>>
<?php wp_body_open(); ?>
<div id="app-root">
  <div id="gcv-layout" style="position:fixed; inset:0; display:flex; gap:0; padding:10px; color:#2d7f08;">
    <div>
      <label for="gcode-input">Enter G-code:</label>
      <textarea id="gcode-input"></textarea>
      <div>
        <button id="plot-btn">Plot</button>
        <label><input type="checkbox" id="plot-up-to-cursor" /> Plot up to cursor</label>
      </div>
    </div>
    <div id="plot" style="flex:1 1 0; min-width:0; min-height:0; position:relative;"></div>
    <div id="tooltip"></div>
  </div>
</div>
<?php wp_footer(); ?>
</body>
</html>

Key idea: the gcv-blank body class means you do not need to edit header.php or footer.php. The template hides them only on this route. Everything else on the site keeps the normal chrome.

3) Keep layout simple and predictable

In your child CSS or in app.css:
/* Left panel */
#gcode-input {
  width: 300px;
  height: 500px;
  margin-top: 20px;
  border: 1px solid #333;
  padding: 10px;
  font-family: monospace;
  background: #1E1E1E;
  color: #2d7f08;
}

/* Plot grows to fill the rest */
#plot {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  height: 100%;
  width: 100%;
  position: relative;
}

/* Tooltip styling */
#tooltip {
  position: absolute;
  background: rgba(0,0,0,0.7);
  color: #fff;
  padding: 5px;
  border-radius: 3px;
  pointer-events: none;
  display: none;
}

The important bits here are min-width: 0 and min-height: 0 on the flex child. Many themes add defaults that fight your layout. Clearing those ensures your plot panel can actually shrink and grow. That makes your getBoundingClientRect() honest.

Compute your fit using rect.width and rect.height, not window size, and not a guessed container size.

4) Enqueue only on the template to keep the site lean

The is_page_template('page-gcode-viewer-blank.php') guard means PIXI and your app script are not loaded across the rest of the site. Faster pages, fewer conflicts, less debugging.

5) Optional hardening and polish

  • Consent and analytics: if you load analytics or AdSense, keep using your consent plugin and let this page inherit it.
  • CSP: if you lock down script sources, add your CDN for PIXI and your child theme path.
  • Dequeue heavy styles on this template: do this only if needed, and only after testing your consent and banner plugins still render usable UI.
  • Accessibility: keep a skip link on normal pages. On the blank template you can hide global nav, but keep focus order sane inside your app.

Why go through all this instead of using the subdomain

The moment you want SEO, analytics, consent, and branding to be consistent without extra work, the child theme approach wins. It keeps the power of WordPress, but gives your canvas a clean room to operate in.

A sizing bug and the fix

This is the bit that cost the most time and was confounding as it was working in the standalone version. I started inspecting and realized that the container and renderer were different sizes and they were both different from the actual plot.

  • The viewer was originally measuring the container and set PIXI’s renderer based on that value. Which works when there is not a ton of CSS headers padding and other layout elements.
  • i then changed it to be based off the renderer and that was worse as PIXI, by design, can render a larger world than the current viewport, which is normal, but my fit logic was using the wrong dimensions at the wrong time.
  • Browser caching of older JS did not help, so sometimes i was running a previous layout path while I was trying to test new logic. so use inspector to verify all code changes in the app as it is running

What worked:

  1. Measure the viewable area directly with plotElement.getBoundingClientRect().
  2. Resize the renderer to that rect before any scale or fit math runs.
  3. Keep rect fresh whenever the container changes size. I use a resize handler that updates both renderer size and any dependent transforms.
  4. Fit to the renderer’s current size when computing scale, not to window or a stale container value.
What that looks like in code
const plotElement = document.getElementById('plot');
let rect = plotElement.getBoundingClientRect();

const app = new PIXI.Application({
  width: rect.width,
  height: rect.height,
  backgroundColor: 0x000000,
  resolution: window.devicePixelRatio || 1,
  antialias: true
});
plotElement.appendChild(app.view);

function resizeApp(){
  const r = plotElement.getBoundingClientRect();
  rect = r;
  app.renderer.resize(Math.max(1, r.width), Math.max(1, r.height));
  app.view.style.width  = r.width + 'px';
  app.view.style.height = r.height + 'px';
}
window.addEventListener('resize', resizeApp);
resizeApp();

That is the heart of it. Measure what the user actually sees, resize the renderer to match, then fit to that, not to the page or an assumed container.


Caching: what I turned off, how it still bit me

I turned off server and CDN caches during development. That however did not disable browser caching, which kept serving older JS to me.

What worked for me:

  • Disabling cache in DevTools Network tab during testing.

Developer notes for fellow tinkerers

  • 0,0 belongs at the bottom left. Keep the world transform pinned when the container resizes.
  • If you use PIXI, do not instantiate with fixed width and height from an element that is not yet laid out. Append, measure with getBoundingClientRect(), resize the renderer, then fit.
  • PIXI’s origin is top left by default. I flipped Y and adjusted G2 and G3 to make arc direction match expectation.
  • If your theme uses content wrappers with max-width and padding, give the app its own wrapper that opts out of article chrome.

Try it and tell me what breaks

The new viewer is live at /gcodeviewer. If you hit a rendering glitch, an off origin, or anything that looks off, drop a comment with your browser and a screenshot. I am collecting edge cases and I would like to turn this into a proper tool.

Thanks for following along. This looked small on paper, but it surfaced real lessons about theme integration and cache strategy. If you are planning a similar move, I hope this saves you a few hours.

Bugs I know of:

  • if you have a circle the scaling does not work: simply need to calculate the resultant points and scale on that. Fix and article coming soon

Comments

Leave a Reply