@collidecreatives/edit-mode
v0.4.0
Published
Tiny browser edit mode overlay for collecting client copy changes.
Maintainers
Readme
@collidecreatives/edit-mode
Tiny browser edit mode overlay for collecting client copy changes.
It lets a client open a page with ?edit=true, click visible text, make copy tweaks, then copy a structured JSON change list.
Install
npm install @collidecreatives/edit-modeQuick start
import { initClientEditMode } from "@collidecreatives/edit-mode";
initClientEditMode({
brandName: "Client Edit Mode",
sessionKey: "mm-edit-mode",
});Open any page with:
https://example.com/about?edit=trueStatic browser script
Use the IIFE build if a site cannot bundle npm packages (e.g. plain WordPress themes):
<script
src="/edit-mode.js"
data-brand-name="Client Edit Mode"
data-session-key="client-edit-mode"
></script>The browser build auto-initialises and reads data-* attributes on its own <script> tag (data-brand-name, data-query-param, data-query-value, data-session-key, data-accent-colour, data-editable-selector, data-skip-selector). It only activates when ?edit=true is present, when the session key is already set, or when you initialise manually with enabled: true.
Framework guides
Every guide below follows the same shape: enqueue/import the package, then gate activation with enabled — computed server-side — for password protection.
WordPress
Enqueue the static IIFE build from your theme's functions.php:
function collide_edit_mode_enqueue() {
wp_enqueue_script(
'collide-edit-mode',
get_template_directory_uri() . '/assets/edit-mode.js',
[],
'1.0.0',
true
);
$password_ok = isset($_GET['edit']) && hash_equals(EDIT_MODE_PASSWORD, $_GET['edit']);
wp_add_inline_script(
'collide-edit-mode',
'window.CollideEditMode.initClientEditMode({ enabled: ' . ($password_ok ? 'true' : 'false') . ', brandName: "Site Edit Mode" });',
'after'
);
}
add_action('wp_enqueue_scripts', 'collide_edit_mode_enqueue');Define EDIT_MODE_PASSWORD in wp-config.php (outside the web root's public reach, never in a theme file committed to a public repo):
define('EDIT_MODE_PASSWORD', getenv('EDIT_MODE_PASSWORD'));Statamic
Import the npm package in your site's Vite entry and check the password in an Antlers/PHP context, exposing only a boolean to the front end:
{{-- resources/views/layout.antlers.html --}}
<script>
window.editModeEnabled = {{ if segment_1 == 'edit' && get:edit == env('EDIT_MODE_PASSWORD') }}true{{ else }}false{{ /if }};
</script>// resources/js/site.js
import { initClientEditMode } from "@collidecreatives/edit-mode";
initClientEditMode({
enabled: window.editModeEnabled,
brandName: "Client Edit Mode",
});EDIT_MODE_PASSWORD lives in .env, never in a versioned Antlers template or the JS bundle.
Next.js
Check the password in a Server Component (App Router) and pass the result to a small Client Component boundary — initClientEditMode touches window/document, so it must run in a "use client" file:
// app/edit-mode.tsx
"use client";
import { useEffect } from "react";
import { initClientEditMode } from "@collidecreatives/edit-mode";
export function EditMode({ enabled }: { enabled: boolean }) {
useEffect(() => {
const instance = initClientEditMode({ enabled, brandName: "Site Edit Mode" });
return () => instance.destroy();
}, [enabled]);
return null;
}// app/layout.tsx
import { cookies } from "next/headers";
import { EditMode } from "./edit-mode";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const password = (await cookies()).get("edit")?.value;
const enabled = password === process.env.EDIT_MODE_PASSWORD;
return (
<html>
<body>
{children}
<EditMode enabled={enabled} />
</body>
</html>
);
}EDIT_MODE_PASSWORD stays server-only in .env (not NEXT_PUBLIC_*), so it never reaches the client bundle.
Astro
Check the password in frontmatter (runs server-side) and pass the boolean into a script:
---
// src/layouts/Layout.astro
const editModeEnabled = Astro.url.searchParams.get("edit") === import.meta.env.EDIT_MODE_PASSWORD;
---
<script define:vars={{ editModeEnabled }}>
import("@collidecreatives/edit-mode").then(({ initClientEditMode }) => {
initClientEditMode({ enabled: editModeEnabled, brandName: "Site Edit Mode" });
});
</script>EDIT_MODE_PASSWORD (no PUBLIC_ prefix) is only readable server-side by Astro, so it never ships in client JS.
Options
initClientEditMode({
enabled: undefined,
queryParam: "edit",
queryValue: "true",
sessionKey: "collide-edit-mode",
storageKey: "collide-edit-mode:draft",
autoSave: true,
brandName: "Edit Mode",
accentColour: "#1e40af",
editableSelector: "h1,h2,h3,h4,h5,h6,p,li,blockquote,figcaption,label,legend,dt,dd,th,td,a,button",
skipSelector: "[data-no-edit],[data-edit-mode-skip],form,input,textarea,select,option,script,style,svg,canvas,iframe",
onCopy: (payload) => console.log(payload),
});Password-protect edit mode
queryValue (e.g. ?edit=happydays instead of ?edit=true) only raises the bar slightly — whatever value you set there ships inside the public JS bundle, so it's not a real secret.
To keep the password server-only, check it in server-rendered code (where the real value can live in an env var, never a client-visible one) and pass the result straight into enabled. The client bundle then never contains the password at all. See the framework guides above for the exact pattern in WordPress, Statamic, Next.js, and Astro — the shared shape is: compare the query string (or a cookie) server-side, then hand the client a plain boolean.
Navigation while editing
Internal links are rewritten to keep ?edit=true when clients move around the site.
Link text is editable. To visit a link while edit mode is on:
- select the link and use Open link in the panel, or
Ctrl/⌘-click the link.
Drafts auto-save before navigation, on each edit, and when the page is hidden.
Opt out in templates
<nav data-no-edit>
...
</nav>Payload
{
"site": "example.com",
"page": "/about",
"pageTitle": "About",
"url": "https://example.com/about?edit=true",
"timestamp": "2026-07-01T00:00:00.000Z",
"changes": [
{
"path": "#hero > h1: \"Old heading\"",
"tag": "H1",
"original": "Old heading",
"new": "New heading"
}
]
}Build
npm run buildOutputs:
dist/index.jsdist/index.cjsdist/index.d.tsdist/browser.global.js
Publish
npm publish --access publicIf publishing privately, configure npm/GitHub Packages first.
