Skip to content

flygoly/nodecode

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NodeCode

Languages: English · 简体中文

High-fidelity, server-side code syntax highlighting for WordPress, powered by Shiki.

Plugin License: MIT Plugin: v0.1.0 Service License: Apache 2.0 Service: v0.1.0 Node.js WordPress

NodeCode highlights your code once, on the server, and serves cached static HTML to visitors. There is no client-side highlighting cost — visitors download pre-rendered, dual-theme HTML, and only a tiny progressive-enhancement script for the copy button, soft-wrap toggle and collapse.

It reuses the exact, high-fidelity TextMate-grammar highlighting that VS Code uses, by running Shiki as a small localhost Node.js service and caching its output in WordPress.


✨ Features

  • Server-side rendering + caching — code is highlighted on save_post, the HTML is cached in post meta, and the frontend just reads it back.
  • VS Code–quality highlighting via Shiki / TextMate grammars.
  • Dual light & dark themes in a single payload, using CSS variables (--nodecode-light / --nodecode-dark).
  • Flexible dark-mode strategy — follow a site selector, follow the visitor's OS, or stay always-light, with safeguards against "double darkening" from stacked dark-mode tools (theme + plugin + Dark Reader).
  • Line annotations — highlight, diff (++/--), focus, and word-highlight via Shiki notation comments.
  • Optional UI — line numbers, language / title bar, copy button, soft-wrap toggle, and collapsible long blocks.
  • Multiple authoring styles — Gutenberg code blocks, classic <pre><code>, and [nodecode] / [code] shortcodes.
  • Graceful degradation — if the service is unreachable, falls back to a plain escaped <pre><code> block; the page never breaks.
  • WP-CLI commands to re-render and flush caches.

🧩 Project structure

This repository contains two independent components under different licenses:

Component Path License What it does
WordPress plugin nodecode-plugin/ MIT Pre-renders code on save, caches HTML, serves dual light/dark themes.
Node.js service nodecode-service/ Apache 2.0 Fastify + Shiki microservice that turns code into themed static HTML.

🏗️ Architecture

flowchart LR
  editor[Author saves post] --> wp[NodeCode plugin save_post]
  wp -->|extract code/lang/meta| api["NodeCode service POST /highlight/batch (localhost)"]
  api -->|themed HTML| wp
  wp -->|cache by hash| meta[(post_meta)]
  visitor[Visitor] --> render[the_content filter]
  render -->|read cached HTML| meta
  render --> page[Static themed HTML + CSS variables]
Loading
  1. On save, the plugin extracts every code block and sends them to the service in one batch request.
  2. The service returns themed HTML; the plugin caches it in post meta keyed by a content+config hash.
  3. On view, the_content swaps in the cached HTML. No external request is made at render time.

📋 Prerequisites

  • Node.js ≥ 18 (for nodecode-service)
  • WordPress ≥ 5.8, PHP ≥ 7.4
  • The service API key in .env must match the plugin setting in Settings → NodeCode

🚀 Installation

1. Run the Node.js service

cd nodecode-service
cp .env.example .env          # set NODECODE_API_KEY (a long random string)
npm install
npm run build
npm start                     # listens on 127.0.0.1:9527
curl http://127.0.0.1:9527/health

For production on Ubuntu, use systemd (recommended) — see Production deployment (Ubuntu + systemd) below.

2. Install the WordPress plugin

  1. Copy nodecode-plugin/ into wp-content/plugins/.
  2. Activate NodeCode in the WordPress admin.
  3. Go to Settings → NodeCode and:
    • Set the Service URL (default http://127.0.0.1:9527) and API Key (must match NODECODE_API_KEY in the service .env).
    • Click Test service connection.
    • Pick Light theme / Dark theme (must be preloaded by the service).
    • Choose your Dark mode strategy and, if using the selector strategy, set the Dark mode CSS selector to match your theme.
    • Enable or disable feature toggles (line numbers, copy button, language label, soft-wrap, collapse).

Dark mode & advanced plugin topics: nodecode-plugin/README.md.

3. Re-render existing posts (optional)

After changing themes or plugin settings, regenerate cached HTML for posts that were saved earlier:

wp nodecode rerender --all

🔄 Upgrading

NodeCode is two components — nodecode-plugin/ and nodecode-service/ — usually updated from the same git repository. Release tags look like nodecode-plugin/v0.1.0 and nodecode-service/v0.1.0 (see the version badges at the top of this README).

Recommended order: upgrade the service first, then the plugin, then re-render cached posts. That way PHP always talks to a running service with a compatible API.

Check installed versions

Component Where to look
Plugin Plugins → Installed Plugins (version under NodeCode), or grep Version nodecode-plugin/nodecode.php in the repo
Service curl -s http://127.0.0.1:9527/health (service must be up), or version in nodecode-service/package.json

Upgrade nodecode-service

Local / manual process

cd nodecode-service
git pull                    # or: git checkout nodecode-service/v0.2.0  (from repo root)
npm ci                      # or: npm install
npm run build
# restart however you run it (npm start, PM2, etc.)
curl http://127.0.0.1:9527/health

If you use a .env file, keep it — new releases may add variables; compare with .env.example. Production should use /etc/nodecode.env (see Configuration); after editing env vars, sudo systemctl restart nodecode-service.

Production (systemd at /var/www/nodecode)

cd /var/www/nodecode
sudo git fetch --tags
# Option A — latest on master:
sudo git pull
# Option B — pin a service release:
# sudo git checkout nodecode-service/v0.2.0 -- nodecode-service

cd nodecode-service
sudo -u www-data npm ci
sudo -u www-data npm run build
sudo systemctl restart nodecode-service
curl http://127.0.0.1:9527/health

Upgrade nodecode-plugin

The plugin is plain PHP — no build step. Replace the folder under wp-content/plugins/.

From a git checkout (typical)

cd /path/to/nodecode          # your clone of this repository
git pull                      # or: git checkout nodecode-plugin/v0.2.0 -- nodecode-plugin

cp -a nodecode-plugin /path/to/wordpress/wp-content/plugins/nodecode-plugin
# production example:
# sudo cp -a /var/www/nodecode/nodecode-plugin /var/www/html/wp-content/plugins/nodecode-plugin
# sudo chown -R www-data:www-data /var/www/html/wp-content/plugins/nodecode-plugin

In wp-admin → Plugins, confirm NodeCode is still active (no re-activation needed if the folder name stays nodecode-plugin). Open Settings → NodeCode and click Test service connection.

ZIP / copy-only installs: unpack the new nodecode-plugin directory over the old one, keeping wp-content/plugins/nodecode-plugin/ as the target path.

Refresh cached HTML after an upgrade

Theme names, plugin version, and service output can change what gets cached. After upgrading either component (especially the plugin or Shiki/themes), regenerate HTML:

wp nodecode rerender --all
# production, from WordPress root:
# sudo -u www-data wp nodecode rerender --all --path=/var/www/html

Optional checks:

wp nodecode health
wp nodecode rerender --post=123    # single post

Monorepo: upgrade both in one pull

If you deploy the full repository at /var/www/nodecode (as in Production deployment):

cd /var/www/nodecode
sudo git pull
cd nodecode-service && sudo -u www-data npm ci && sudo -u www-data npm run build
sudo systemctl restart nodecode-service
sudo cp -a /var/www/nodecode/nodecode-plugin /var/www/html/wp-content/plugins/nodecode-plugin
sudo chown -R www-data:www-data /var/www/html/wp-content/plugins/nodecode-plugin
sudo -u www-data wp nodecode rerender --all --path=/var/www/html

More service notes: nodecode-service/README.md. Plugin details: nodecode-plugin/README.md.

🏭 Production deployment (Ubuntu + systemd)

This guide installs NodeCode on an Ubuntu server that already runs WordPress. The Node service listens on 127.0.0.1:9527 so only local PHP can reach it. systemd keeps the service running after reboots and restarts it if the process crashes.

For local development on your laptop, use dev/README.md instead.

Architecture

flowchart LR
  visitor[Visitor] --> nginx[Nginx_or_Apache]
  nginx --> php[WordPress_PHP]
  php -->|save_post HTTP POST| node[NodeCode_127.0.0.1_9527]
  node -->|themed_HTML| php
  php --> meta[(post_meta_cache)]
  visitor -->|view_post reads cache| php
Loading

Visitors never call port 9527 directly. Highlighting runs when an author saves a post.

Prerequisites

  • Ubuntu 20.04 or newer (22.04 LTS recommended)
  • WordPress 5.8+, PHP 7.4+ (same machine as the Node service)
  • sudo access, git, curl
  • WordPress web root (examples use /var/www/html — adjust to your site)

Step 1: Install Node.js 20 LTS

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v    # must be v18 or newer
npm -v

Step 2: Deploy nodecode-service

cd /var/www
sudo git clone https://github.com/flygoly/nodecode.git
cd nodecode/nodecode-service
sudo npm ci
sudo npm run build

Confirm dist/server.js exists. Set ownership so the WordPress user can read the app (often www-data):

sudo chown -R www-data:www-data /var/www/nodecode/nodecode-service

Step 3: Configure /etc/nodecode.env

Do not rely on a .env file inside the web tree. Use a root-owned env file for systemd:

sudo cp deploy/nodecode.env.example /etc/nodecode.env
sudo nano /etc/nodecode.env

Set at minimum:

# Generate a key:
openssl rand -hex 32
NODECODE_HOST=127.0.0.1
NODECODE_PORT=9527
NODECODE_API_KEY=<paste-the-random-key-here>
NODECODE_LIGHT_THEME=github-light
NODECODE_DARK_THEME=github-dark

Lock down permissions:

sudo chmod 600 /etc/nodecode.env
sudo chown root:root /etc/nodecode.env

Security: keep NODECODE_HOST=127.0.0.1. Do not expose port 9527 on a public firewall; only WordPress on the same host should connect.

Step 4: Install and enable systemd (boot persistence)

Edit paths if your clone is not under /var/www/nodecode/nodecode-service:

sudo nano /var/www/nodecode/nodecode-service/deploy/nodecode-service.service
# Check WorkingDirectory and ExecStart (/usr/bin/node — run `which node` if unsure)

Install the unit:

sudo cp /var/www/nodecode/nodecode-service/deploy/nodecode-service.service \
  /etc/systemd/system/nodecode-service.service
sudo systemctl daemon-reload
sudo systemctl enable nodecode-service    # start on boot — required
sudo systemctl start nodecode-service
sudo systemctl status nodecode-service

Verify:

curl http://127.0.0.1:9527/health
# {"status":"ok","loaded":...}

View logs:

sudo journalctl -u nodecode-service -f
Command Purpose
sudo systemctl enable nodecode-service Register for boot (without this, a reboot stops NodeCode)
sudo systemctl restart nodecode-service Apply config or code changes
sudo systemctl is-enabled nodecode-service Should print enabled

The unit uses Restart=always so the process is restarted after crashes.

Step 5: Install the WordPress plugin

sudo cp -a /var/www/nodecode/nodecode-plugin /var/www/html/wp-content/plugins/nodecode-plugin
sudo chown -R www-data:www-data /var/www/html/wp-content/plugins/nodecode-plugin

In wp-admin:

  1. Plugins → Installed Plugins — activate NodeCode.
  2. Settings → NodeCode:
    • Service URL: http://127.0.0.1:9527
    • API Key: same value as NODECODE_API_KEY in /etc/nodecode.env
    • Click Test service connection
    • Choose Light theme / Dark theme (must match preloaded service themes)
    • Set Dark mode strategy and CSS selector for your theme

See nodecode-plugin/README.md for dark-mode details.

Step 6: Verify highlighting

  1. Create or edit a post with a Code block or [nodecode] shortcode; Update the post.
  2. View the post on the front end — syntax colors and UI should appear (the block editor still shows raw source; that is expected).
  3. Optional — from the WordPress root:
cd /var/www/html
sudo -u www-data wp nodecode health
sudo -u www-data wp nodecode rerender --all

Step 7: Reboot check

Confirm the unit is enabled, then reboot the server:

sudo systemctl is-enabled nodecode-service
sudo reboot

After login:

sudo systemctl status nodecode-service
curl http://127.0.0.1:9527/health

In wp-admin, run Test service connection again.

Related services (WordPress stack)

NodeCode does not replace your web stack. Ensure these are also enabled for boot:

sudo systemctl enable nginx          # or apache2
sudo systemctl enable php8.2-fpm     # adjust PHP version
sudo systemctl enable mysql          # or mariadb

Troubleshooting

Symptom What to check
Test connection fails sudo systemctl status nodecode-service; curl http://127.0.0.1:9527/health; journalctl -u nodecode-service -n 50
Works until reboot Run sudo systemctl enable nodecode-service (not only start)
API key / 401 errors NODECODE_API_KEY in /etc/nodecode.env must match Settings → NodeCode; restart after edits: sudo systemctl restart nodecode-service
dist/server.js missing cd /var/www/nodecode/nodecode-service && sudo -u www-data npm run build
Permission errors www-data must read dist/ and node_modules/ under nodecode-service
No highlight after save PHP must reach localhost (wp_remote_post); check wp-content/debug.log

Updating NodeCode

Full component-by-component steps: Upgrading. Quick path when the whole repo lives at /var/www/nodecode:

cd /var/www/nodecode
sudo git pull
cd nodecode-service
sudo -u www-data npm ci
sudo -u www-data npm run build
sudo systemctl restart nodecode-service
sudo cp -a /var/www/nodecode/nodecode-plugin /var/www/html/wp-content/plugins/nodecode-plugin
sudo chown -R www-data:www-data /var/www/html/wp-content/plugins/nodecode-plugin
sudo -u www-data wp nodecode rerender --all --path=/var/www/html

⚙️ Configuration

Service (environment variables)

Variable Default Description
NODECODE_HOST 127.0.0.1 Bind address. Keep on localhost.
NODECODE_PORT 9527 Port.
NODECODE_API_KEY (empty) Shared secret; must match the plugin setting.
NODECODE_LIGHT_THEME github-light Light theme name.
NODECODE_DARK_THEME github-dark Dark theme name.
NODECODE_EXTRA_LANGS (empty) Extra languages to preload, comma separated.
NODECODE_MAX_CODE_LENGTH 200000 Max characters accepted per code block.

Plugin (Settings → NodeCode)

Service URL, API key, light/dark themes, dark-mode strategy + selector, and feature toggles (line numbers, copy, language label, soft-wrap, collapse).

✍️ Authoring code blocks

NodeCode detects code blocks automatically from post content. All three styles get the same Shiki highlighting on the frontend; they differ in how you write and what you can configure per block.

Choosing a style

Style Where to write Per-block title Per-block linenumbers / wrap Line markers [!code ...]
A. Gutenberg Code Code block (WordPress core) Only via data-title in code view Follow Settings → NodeCode Yes
B. <pre><code> Custom HTML block data-title on <pre> or <code> Global defaults; wrap off per block Yes
C. [nodecode] Shortcode or Custom HTML title / file attribute linenumbers / wrap attributes Yes

Summary: Highlighting power is the same. Use [nodecode] when you need per-block title, line numbers, or wrap. Use the Code block for everyday posts with global settings.

Do not paste [nodecode]...[/nodecode] inside a Code block — WordPress will treat it as plain text. Use a Shortcode block instead.

A. Gutenberg code block

WordPress core block (core/code). NodeCode does not register a separate block.

  1. Add a Code block, pick the language in the toolbar, paste your snippet (including // [!code ...] markers).
  2. After save, stored content looks like:
<!-- wp:code {"language":"javascript"} -->
<pre class="wp-block-code"><code class="language-javascript">const legacy = false // [!code --]
const migrated = true // [!code ++]
console.log(migrated) // [!code highlight]</code></pre>
<!-- /wp:code -->

Important:

  • class="language-xxx" must match the language in the <!-- wp:code --> comment, or Gutenberg may show an invalid block warning.
  • Optional title bar: add data-title="demo.js" on <code> in the block’s code view (not exposed in the visual UI).
  • Line numbers and wrap use global plugin settings unless you switch to [nodecode].

B. Classic editor / custom HTML

<pre><code class="language-php" data-title="functions.php">&lt;?php
add_action( 'init', function () {
  do_action( 'demo_hook' ); // [!code highlight]
} );</code></pre>

Legacy SyntaxHighlighter-style classes are also supported, e.g. class="brush: js".

C. Shortcodes

Minimal example with all shortcode attributes:

[nodecode lang="js" title="example.js" linenumbers="true" wrap="false"]
const x = 1
[/nodecode]

Aliases (closing tag must match the opening tag):

  • [code lang="php"] ... [/code]
  • [shiki lang="typescript"] ... [/shiki] (legacy)
Attribute Description
lang / language / lng Language ID (defaults to text if omitted)
title / file Text shown in the block title bar
linenumbers true, false, 1, yes, on, etc.
wrap Enable soft line wrap for this block

Full shortcode example (all attributes and markers)

Copy into a Shortcode block (or Custom HTML). After Update, view the post on the frontend.

[nodecode lang="typescript" title="full-demo.ts" linenumbers="true" wrap="true"]
// [!code highlight:2]
import { readFile } from 'node:fs/promises'

const theme = 'github-light'
const dark = 'github-dark'

async function loadConfig(path: string) {
  const raw = await readFile(path, 'utf8') // [!code highlight]
  const legacy = { version: 1 } // [!code --]
  const config = { version: 2, theme, dark } // [!code ++]
  applyTheme(config.theme) // [!code focus]
  log(config.theme) // [!code word:theme]
  return config
}

export { loadConfig }
[/nodecode]
Part Meaning
lang="typescript" Shiki language
title="full-demo.ts" Title bar label
linenumbers="true" Show line numbers for this block
wrap="true" Soft-wrap + wrap toggle for this block
// [!code highlight:2] Highlight the next 2 lines (Shiki v3)
// [!code highlight] Highlight this line
// [!code --] / ++ Diff removed / added line
// [!code focus] Focus line (blur others until hover)
// [!code word:theme] Highlight occurrences of theme

Same snippet, three ways

Equivalent JavaScript (one highlight, one ++). Pick one authoring style.

1. Code block — paste in the editor:

const legacy = false // [!code --]
const migrated = true // [!code ++]
console.log(migrated) // [!code highlight]

2. Shortcode:

[nodecode lang="javascript" title="demo.js" linenumbers="true"]
const legacy = false // [!code --]
const migrated = true // [!code ++]
console.log(migrated) // [!code highlight]
[/nodecode]

3. Custom HTML:

<pre><code class="language-javascript" data-title="demo.js">const legacy = false // [!code --]
const migrated = true // [!code ++]
console.log(migrated) // [!code highlight]</code></pre>

📝 Line annotations (Shiki notation)

For a single block that uses every marker type together, see Full shortcode example (all attributes and markers).

Add special comments inside your code to mark lines. Comment syntax depends on the language (// for JS/TS, # for Python, etc.):

Marker Effect
// [!code highlight] Highlight the line
// [!code ++] Diff: added line
// [!code --] Diff: removed line
// [!code focus] Focus this line (others blur until hover)
// [!code word:token] Highlight occurrences of token

Example (JavaScript):

console.log('a') // [!code highlight]
const added = 1  // [!code ++]
const removed = 0 // [!code --]
doThing()        // [!code focus]
const token = 1  // [!code word:token]

Annotations appear as plain comments in the editor and in stored post content. The highlighted result is produced when the post is saved (server render) and shown on the frontend from cached HTML.

💾 How saving and caching work

sequenceDiagram
  participant Author
  participant WP as WordPress_plugin
  participant Svc as NodeCode_service
  participant Meta as post_meta

  Author->>WP: save_post
  WP->>WP: Parser_collect_blocks
  WP->>Svc: POST_highlight_batch
  Svc-->>WP: themed_HTML
  WP->>Meta: _nodecode_cache_by_hash
  Note over Author,Meta: Frontend_the_content_reads_cache_no_Shiki_call
Loading
  • Each block is cached under a hash of code + language + title + flags + config version. Changing themes or plugin version changes config version — run wp nodecode rerender to refresh old posts.
  • If the service is down when you save, that block is not written to meta. On the frontend the plugin may try a one-off live render (transient cache), then fall back to a plain escaped <pre><code>.
  • Already published posts with cached HTML keep working when the service is offline. Only newly saved or uncached blocks may show the plain fallback.

🖥️ Editor vs frontend

Where What you see
Block / classic editor Raw source code, including // [!code ...] comments — no Shiki colors
Published frontend Cached themed HTML, line numbers, copy button, annotations, light/dark switch

This is expected: highlighting runs on the server at save time, not inside the WordPress editor.

🔧 Troubleshooting

Problem What to check
Gutenberg invalid block on a code block <!-- wp:code {"language":"..."} --> matches class="language-..." on <code>
No highlighting on the frontend Service curl .../health, plugin Test service connection, re-save the post or wp nodecode rerender
Stale colors after theme change wp nodecode rerender --all
Service down, old post Cached blocks still render; uncached blocks may fall back to plain <pre><code>

CLI helpers:

wp nodecode health
wp nodecode rerender --post=123
wp nodecode flush

🛠️ WP-CLI

wp nodecode rerender --all
wp nodecode rerender --post=123
wp nodecode rerender --all --post_type=post,page
wp nodecode flush          # clear all cached HTML + transients
wp nodecode health         # check the service connection

🧪 Local development

To try NodeCode end-to-end without committing WordPress core, use the portable scripts in dev/:

bash dev/bootstrap.sh
cd nodecode-service && npm install && npm run build && cd ..
bash dev/setup.sh
bash dev/start.sh

Downloads land in gitignored local-wp/; demo admin is admin / admin123 on http://localhost:8765.

📦 Repository layout

dev/                                 Local WP bootstrap / setup / start (tracked)
nodecode-service/                    Node.js highlight microservice (Apache 2.0)
  src/
    config.ts                        env config loader
    highlighter.ts                   Shiki highlighter (dual theme + transformers)
    transformers.ts                  line-number + metadata transformers
    server.ts                        Fastify routes (/highlight, /batch, /health)
  deploy/nodecode-service.service    systemd unit (boot + restart)
  deploy/nodecode.env.example      template for /etc/nodecode.env
  ecosystem.config.cjs               PM2 config
  LICENSE                            Apache License 2.0

nodecode-plugin/                     WordPress plugin (MIT)
  nodecode.php                       entry + autoloader
  includes/                          Settings, Client, Parser, Renderer, Assets, Admin, CLI
  assets/css/nodecode.css            frontend styles (line numbers, diff, collapse, ...)
  assets/js/nodecode.js              copy / wrap toggle / collapse / dark-fade sync
  uninstall.php
  LICENSE                            MIT License

LICENSE                              NodeCode Multi-License Agreement (overview)

📄 License

NodeCode is distributed under a multi-license agreement — see LICENSE:

  • The WordPress plugin (nodecode-plugin/) is licensed under the MIT License.
  • The Node.js service (nodecode-service/) is licensed under the Apache License, Version 2.0.

Copyright (c) 2026 flygoly.

🙏 Credits

Syntax highlighting is powered by Shiki and @shikijs/transformers, distributed under the MIT License by their respective authors.

About

High-fidelity, server-side code syntax highlighting for WordPress, powered by Shiki.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors