Skip to content

Commit 9e4c74a

Browse files
myleshortonclaude
andauthored
wp-plugin: rebrand to Unbounded, fix fresh-install warning, add a demo that cannot rot (#419)
* wp-plugin: rebrand to Unbounded, fix fresh-install warning The product is just "Unbounded" now, so update the nine user-facing strings: plugin name, admin menu, settings page, meta box, and field descriptions. The <browsers-unbounded> element name deliberately stays. It is the widget's public API -- ui/src/index.tsx only matches "lantern-network" (legacy) or "browsers-unbounded", so renaming the tag would silently render nothing. The option key and admin slug stay for the same reason. Also stop interpolating $options['layout'] and ['theme'] unguarded. On a fresh install nobody has saved settings yet, get_option() returns false, and PHP 8 prints "Trying to access array offset on false" into the page while emitting empty data attributes. Fall back to banner/dark and pass both through esc_attr(), since the registered sanitizer is still a no-op @todo and these land in HTML attributes. Verified against WordPress 6.x + PHP 8.2 with WP_DEBUG on: the old version warns and emits data-layout='', the new one is clean and defaults correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lh3TcyWL8MMtTYPo7H2buS * wp-plugin: add a Playground-based demo site Every previous demo of this plugin has rotted. The Heroku instance 404s, the wordpress.com trial expired, and the S3 zip that try.wpdemo.net installs has served the February 2024 build ever since -- a version whose front-end hook predates the homepage and posts settings, so a demo created from it today installs a plugin that silently ignores half the options its own settings screen offers. WordPress Playground removes the server from the equation: WordPress compiled to WebAssembly, booted in the visitor's browser, plugin installed and activated from a blueprint. Nothing to host, nothing to expire, nothing to keep patched. Verified end to end -- the widget renders inside it, globe canvas and connect switch included. build.sh generates wp-plugin.zip from the plugin source rather than committing an archive, so the demo cannot drift from the tree the way the S3 object did. index.html derives the zip URL from location.origin, so the same files work on a preview deployment, production, or a custom domain with no edits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lh3TcyWL8MMtTYPo7H2buS * wp-plugin: build the demo zip with python, not zip(1) Cloudflare's Pages build image has no zip binary, so the first preview deployment failed at the preflight check. python3 is present in both that image and locally, so zipfile keeps CI and local builds on one code path rather than adding a dependency to the build environment. Archive layout is unchanged -- same top-level wp-plugin/ directory entry and the same two files -- and the result still installs and activates cleanly in WordPress with the widget rendering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lh3TcyWL8MMtTYPo7H2buS * wp-plugin: address PR review feedback Build the zip link with DOM APIs instead of innerHTML. The reported XSS path does not actually open -- location.pathname normalises " < > to %22 %3C %3E, so it cannot carry raw HTML metacharacters -- but concatenating a URL into innerHTML is a fragile pattern with no upside here, and createElement plus textContent renders identically. Fix three stale docs while here: - the meta box is its own box in the editor sidebar, not a field inside Page Attributes, so the setup steps pointed readers somewhere that does not exist - the plugin header still described the widget as applying "to selected pages", predating the homepage and posts settings it now also supports - a sentence missing its verb, and a fence missing its language (MD040) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lh3TcyWL8MMtTYPo7H2buS * wp-plugin: put the demo widget above the fold The blueprint set location=footer, which hooks wp_footer and renders the widget below the whole theme -- header, title, content, footer -- so a visitor landed on the demo and saw stock WordPress with the thing they came to look at off screen. location=header hooks wp_head instead. The element is not valid head content, so the browser closes head and hoists it, landing it as the first child of body: measured at 32px from the top, 630px tall, in an 818px viewport. This is also the plugin's own default. The blueprint was overriding it for no reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lh3TcyWL8MMtTYPo7H2buS --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5256465 commit 9e4c74a

8 files changed

Lines changed: 307 additions & 17 deletions

File tree

ui/wp-plugin/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Browsers Unbounded WordPress Plugin
1+
# Unbounded WordPress Plugin
22

3-
Quick guide to get the Browsers Unbounded plugin running with Docker
3+
Quick guide to get the Unbounded plugin running with Docker
44

55
## Setup
66

@@ -21,11 +21,11 @@ Quick guide to get the Browsers Unbounded plugin running with Docker
2121

2222
4. **Activate Plugin:**
2323
- Go to `http://localhost:8000/wp-admin/plugins.php`
24-
- Find the `Browsers Unbounded` plugin and click `Activate`
24+
- Find the `Unbounded` plugin and click `Activate`
2525

2626
5. **Test Plugin:**
2727
- Go to `http://localhost:8000/wp-admin/admin.php?page=browsers-unbounded-settings`
2828
- You should see the plugin settings page
2929
- Configure the plugin settings as needed
30-
- Create a new page and check the `Enable Browsers Unbounded` box in the `Page Attributes` section
30+
- Create a new page and tick `Enable Unbounded on this page` in the editor sidebar
3131
- View the page and you should see the widget in action

ui/wp-plugin/browsers-unbounded-plugin.php

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?php
22
/**
3-
* Plugin Name: Browsers Unbounded
4-
* Description: Browsers Unbounded widget to selected pages with editable theme, layout and location.
5-
* Version: 1.0
6-
* Author: Echo
3+
* Plugin Name: Unbounded
4+
* Description: Unbounded widget with editable theme, layout and location. Enable it per page, on all posts, or on the homepage.
5+
* Version: 1.1
6+
* Author: Lantern
77
*/
88

99
// hooks for the admin ui
@@ -27,7 +27,7 @@ function browsers_unbounded_add_hooks_based_on_location() {
2727

2828
// menu item for settings page
2929
function browsers_unbounded_plugin_menu() {
30-
add_menu_page('Browsers Unbounded Settings', 'Browsers Unbounded', 'manage_options', 'browsers-unbounded-settings', 'browsers_unbounded_plugin_settings_page');
30+
add_menu_page('Unbounded Settings', 'Unbounded', 'manage_options', 'browsers-unbounded-settings', 'browsers_unbounded_plugin_settings_page');
3131
}
3232

3333
// settings for storing plugin options
@@ -51,7 +51,7 @@ function browsers_unbounded_options_sanitize($options) {
5151
function browsers_unbounded_plugin_settings_page() {
5252
?>
5353
<div class="wrap">
54-
<h2>Browsers Unbounded Settings</h2>
54+
<h2>Unbounded Settings</h2>
5555
<form method="post" action="options.php">
5656
<?php settings_fields('browsers_unbounded_options_group'); ?>
5757
<?php do_settings_sections('browsers-unbounded-settings'); ?>
@@ -71,7 +71,7 @@ function browsers_unbounded_layout_callback() {
7171
<option value='panel' <?php selected($layout, 'panel'); ?>>Panel</option>
7272
<option value='floating' <?php selected($layout, 'floating'); ?>>Floating</option>
7373
</select>
74-
<p class="description">Select Browsers Unbounded layout.</p>
74+
<p class="description">Select Unbounded layout.</p>
7575
<?php
7676
}
7777

@@ -85,7 +85,7 @@ function browsers_unbounded_theme_callback() {
8585
<option value='dark' <?php selected($theme, 'dark'); ?>>Dark</option>
8686
<option value='auto' <?php selected($theme, 'auto'); ?>>Auto</option>
8787
</select>
88-
<p class="description">Select Browsers Unbounded theme.</p>
88+
<p class="description">Select Unbounded theme.</p>
8989
<?php
9090
}
9191

@@ -97,7 +97,7 @@ function browsers_unbounded_location_callback() {
9797
<option value='header' <?php selected($location, 'header'); ?>>Header</option>
9898
<option value='footer' <?php selected($location, 'footer'); ?>>Footer</option>
9999
</select>
100-
<p class="description">Select where to add Browsers Unbounded.</p>
100+
<p class="description">Select where to add Unbounded.</p>
101101
<?php
102102
}
103103

@@ -122,7 +122,7 @@ function browsers_unbounded_posts_callback() {
122122

123123
// meta box to the page editor to enable the unbounded widget
124124
function browsers_unbounded_add_meta_box() {
125-
add_meta_box('browsers-unbounded-enable', 'Enable Browsers Unbounded', 'browsers_unbounded_meta_box_callback', 'page', 'side');
125+
add_meta_box('browsers-unbounded-enable', 'Enable Unbounded', 'browsers_unbounded_meta_box_callback', 'page', 'side');
126126
}
127127

128128
// renders meta box in the page editor
@@ -131,7 +131,7 @@ function browsers_unbounded_meta_box_callback($post) {
131131
$value = get_post_meta($post->ID, '_browsers_unbounded_enable', true);
132132
echo '<label for="browsers_unbounded_field">';
133133
echo '<input type="checkbox" id="browsers_unbounded_field" name="browsers_unbounded_field" value="1"' . checked($value, 1, false) . ' />';
134-
echo ' Enable Browsers Unbounded on this page';
134+
echo ' Enable Unbounded on this page';
135135
echo '</label> ';
136136
}
137137

@@ -159,7 +159,13 @@ function browsers_unbounded_add_element_and_script() {
159159
// script
160160
echo '<script defer="defer" src="https://embed.lantern.io/static/js/main.js"></script>';
161161

162-
// element
163-
echo "<browsers-unbounded data-layout='{$options['layout']}' data-theme='{$options['theme']}' style='width: 100%;'></browsers-unbounded>";
162+
// element — tag name is the widget's public API, so it stays "browsers-unbounded"
163+
$layout = isset($options['layout']) ? $options['layout'] : 'banner';
164+
$theme = isset($options['theme']) ? $options['theme'] : 'dark';
165+
printf(
166+
"<browsers-unbounded data-layout='%s' data-theme='%s' style='width: 100%%;'></browsers-unbounded>",
167+
esc_attr($layout),
168+
esc_attr($theme)
169+
);
164170
}
165171
}

ui/wp-plugin/demo-site/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/

ui/wp-plugin/demo-site/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Unbounded WordPress plugin — demo site
2+
3+
A one-click live demo of the plugin. No server runs WordPress: the landing page
4+
links to [WordPress Playground](https://playground.wordpress.net), which boots
5+
WordPress compiled to WebAssembly inside the visitor's browser, installs
6+
`wp-plugin.zip`, activates it, and lands on a page with the widget embedded.
7+
8+
## Layout
9+
10+
| File | Role |
11+
|---|---|
12+
| `index.html` | Landing page. Builds the Playground link at runtime from `location.origin`, so it works on any domain with no edits. |
13+
| `blueprint.json` | Playground setup steps. `PLUGIN_ZIP_URL` is substituted by `index.html`. |
14+
| `_headers` | Permissive CORS. Belt-and-braces: Playground fetches plugin zips through its own proxy, so this is not currently load-bearing. |
15+
| `build.sh` | Assembles `dist/`, generating `wp-plugin.zip` from the plugin source. |
16+
17+
`dist/` is generated and gitignored.
18+
19+
## Cloudflare Pages settings
20+
21+
The project is connected to this repo, so a push to `main` publishes and every PR
22+
gets a preview URL.
23+
24+
```text
25+
Build command: bash ui/wp-plugin/demo-site/build.sh
26+
Build output directory: ui/wp-plugin/demo-site/dist
27+
Root directory: /
28+
```
29+
30+
Optionally set *Build watch paths* to `ui/wp-plugin/*` so unrelated pushes do not
31+
trigger a rebuild.
32+
33+
## Why the zip is built, not committed
34+
35+
The previously published archive lived at `unbounded.s3.amazonaws.com/wp-plugin.zip`
36+
and was uploaded by hand in February 2024. The plugin source kept changing; that
37+
object did not. By 2026 it still shipped the pre-rebrand build whose front-end hook
38+
predated the homepage and posts settings entirely — so a demo created from it
39+
installed a plugin that silently ignored half the options the settings screen
40+
offered. `build.sh` regenerates the zip on every deploy so the demo can only ship
41+
what is actually in the tree.
42+
43+
## Testing locally
44+
45+
Run `build.sh`, then serve `dist/` over **HTTPS** — Playground is an HTTPS origin, and a
46+
plugin zip on `http://localhost` is blocked as mixed content, which surfaces only as
47+
a Playground that hangs on "Loading Playgrounds". A preview deployment is usually the
48+
faster way to check a change.
49+
50+
For testing the plugin against real WordPress and MySQL instead, see the
51+
`docker-compose.yml` one directory up.

ui/wp-plugin/demo-site/_headers

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/*
2+
Access-Control-Allow-Origin: *
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
3+
"landingPage": "/",
4+
"preferredVersions": { "php": "8.3", "wp": "latest" },
5+
"login": true,
6+
"steps": [
7+
{
8+
"step": "installPlugin",
9+
"pluginData": { "resource": "url", "url": "PLUGIN_ZIP_URL" },
10+
"options": { "activate": true }
11+
},
12+
{
13+
"step": "runPHP",
14+
"code": "<?php require_once '/wordpress/wp-load.php'; $id = wp_insert_post(array('post_type'=>'page','post_title'=>'Unbounded Demo','post_status'=>'publish','post_content'=>'<p>This page is running the Unbounded WordPress plugin. The widget below turns this browser into a volunteer proxy that helps people reach the open internet.</p>')); update_post_meta($id,'_browsers_unbounded_enable','1'); update_option('browsers_unbounded_options', array('layout'=>'banner','theme'=>'dark','location'=>'header','homepage'=>'on')); update_option('show_on_front','page'); update_option('page_on_front',$id); update_option('blogname','Unbounded Demo');"
15+
}
16+
]
17+
}

ui/wp-plugin/demo-site/build.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
# Assemble the deployable demo site into ./dist.
3+
#
4+
# wp-plugin.zip is built from the plugin source next door rather than committed.
5+
# The hand-uploaded copy on S3 sat at the Feb 2024 build for two and a half years
6+
# while the source moved on, so a demo created from it installed a plugin that
7+
# silently ignored half its own settings. Generating the zip here means the demo
8+
# can only ever ship what is actually in the tree.
9+
set -euo pipefail
10+
11+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12+
plugin_dir="$(dirname "$here")"
13+
out="$here/dist"
14+
15+
command -v python3 >/dev/null || {
16+
echo "build.sh needs python3, which is not on PATH in this build image" >&2
17+
exit 1
18+
}
19+
20+
rm -rf "$out"
21+
mkdir -p "$out/stage/wp-plugin"
22+
23+
# Mirror the layout of the published archive: a single top-level wp-plugin/ dir.
24+
for f in browsers-unbounded-plugin.php README.md; do
25+
cp "$plugin_dir/$f" "$out/stage/wp-plugin/$f"
26+
done
27+
28+
# zipfile rather than the zip(1) binary: Cloudflare's Pages build image ships
29+
# python3 but not zip, and this keeps the local and CI builds on one code path.
30+
python3 - "$out/stage" "$out/wp-plugin.zip" <<'PY'
31+
import os, sys, zipfile
32+
33+
stage, target = sys.argv[1], sys.argv[2]
34+
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as z:
35+
for root, dirs, files in sorted(os.walk(stage)):
36+
dirs.sort()
37+
rel = os.path.relpath(root, stage)
38+
if rel != ".":
39+
z.writestr(rel + "/", "")
40+
for name in sorted(files):
41+
path = os.path.join(root, name)
42+
z.write(path, os.path.relpath(path, stage))
43+
PY
44+
rm -rf "$out/stage"
45+
46+
cp "$here/index.html" "$here/blueprint.json" "$here/_headers" "$out/"
47+
48+
version=$(sed -n 's/^ \* Version: *//p' "$plugin_dir/browsers-unbounded-plugin.php" | head -1)
49+
echo "built demo site -> $out (plugin v${version:-unknown}, zip $(wc -c < "$out/wp-plugin.zip") bytes)"

0 commit comments

Comments
 (0)