WebProCMS lets editors copy a row — or every section of a whole page — on one live site and paste it into the editor of a different live WebProCMS install. Design a CTA row on Site A, press Ctrl+C, switch to Site B's editor, press Ctrl+V, and the row arrives with its text, styling, and images intact. Copying "all sections" moves an entire page's worth of content between installs in two keystrokes.
It rides the operating-system clipboard, so there's no export file to manage, no server-to-server connection to configure, and no shared account — the content travels in the clipboard as a self-describing JSON payload, and referenced images are pulled across over HTTPS at paste time.
Status: shipped. The envelope, OS-clipboard transport, paste import, SSRF-guarded media fetch, and the file-based page-bundle sibling are all live (see "Where this lives in the code" below). Envelope version 2 (RowDoc inversion, 2026-07-10) made the payload doc-only: each row's structure travels as its RowDocument fragment tree (
doc.nodes), never as blade text — the import side compiles the tree viaRowCompilerand, for editors below the admin-only Code tab, gates the compiled bytes against the row's design-library template baseline before anything is written. Version 1 (blade-carrying) envelopes are refused with a "copy it again" message.
Why this matters
The same git repo can power many client installs, each with its own database. That's exactly why cloning the repo doesn't move content between installs — the pages and their copy live in each site's content_overrides table, not in the code. Cross-site copy/paste closes that gap at the unit editors actually think in: a row, or a page's worth of rows.
Concrete wins:
- Reuse a polished row everywhere. Build a CTA, pricing block, or testimonial section once on a real site, then drop it into any other site without rebuilding it.
- Move a page between installs. Use the editor's "Copy all sections" to lift a full page's content and paste it as a brand-new page on the target.
- No infrastructure. No SFTP, no DB export, no API keys between sites. If you can open both editors in your browser, you can move content between them.
When it works
The clean, frictionless path is live site → live site, because the browser's asynchronous Clipboard API (navigator.clipboard) requires a secure context — i.e. HTTPS. Production WebProCMS installs are served over SSL, so both the copy and the paste side have full native clipboard access. Ctrl/Cmd+C and Ctrl/Cmd+V "just work" across the two tabs.
A local dev install is typically served over plain HTTP (e.g. http://webprocms.test), where the async clipboard API is unavailable. Moving content out of a local install therefore uses the file-based path instead (see Relationship to other features) rather than the native clipboard. Local → live and live → live both remain possible; only the transport differs.
How it works
Five pieces:
-
A portable content payload. Copy serializes the selected row(s) into a versioned, self-describing JSON envelope — each row's structure as a RowDocument fragment tree (
doc.nodes, version 2; never blade text), itscontent_overridesvalues (text, classes, toggles, links), and a manifest of the images it references. Crucially, the payload carries the values, not just a reference to them, so it stands alone on a machine that has never seen the source database. -
OS-clipboard transport. The envelope is written to the operating-system clipboard via the async Clipboard API. Because it lives on the OS clipboard (not site-scoped
localStorage), it crosses domains, browser tabs, and even machines with clipboard sync. Same-origin paste keeps using the fastlocalStoragepath; cross-origin paste reads the OS clipboard. -
Envelope-aware paste. On Ctrl/Cmd+V outside a text field, the editor reads the clipboard, parses the JSON, and checks for the WebProCMS envelope marker. It routes by
kind: arow/rowspayload appends sections into the page currently open in the editor; apagepayload offers Import as new page. The import compiles each row's document tree server-side (RowCompiler) — envelope text is never adopted — and for editors below the admin-only Code tab every compiled row must pass theBladeSurgerySafetyconstruct gate anchored on its design-library template source before anything is written. -
Image fetch-over-HTTPS. Image references in the payload are absolute URLs into the source site's public media. At paste time the target site downloads each one, runs it through the normal upload pipeline (stored under
Y/m/at a server-minted name whose extension comes from the decoded bytes, registered as aMediaItem), and rewrites the pasted content to point at the target's own copy. An image whose path already exists in the target library is reused rather than re-fetched. -
New-page assembly. A
pagepayload creates a real page on the target: a⚡{slug}.blade.phpfile, its route, its SEO metadata, all of its rows' overrides, and a freshly compiled page-data sidecar. It does not auto-add the page to navigation — you place it where you want it.
What travels, and what doesn't
| Content | Travels in the payload? | Notes |
|---|---|---|
| Row structure (document tree) | ✅ | The row's full structure travels as its RowDocument fragment tree and is compiled to blade on the target, so it renders as long as the target has the shared <x-dl.*> components — which every install does, since they're in the repo. |
| Text, headings, button labels | ✅ | Stored as content_overrides values, embedded directly. |
Styling (*_classes), toggles, links, IDs |
✅ | Same — embedded values, fully portable. |
| Images (hero, gallery, icons, backgrounds) | ✅ (fetched) | Paths don't transfer literally; the image files are pulled from the source over HTTPS and re-imported into the target's media library. |
| Page metadata (SEO title/description, slug) | ✅ (page payloads only) | Only when copying a whole page via "Copy all sections". |
| Navigation placement | ❌ | A pasted/imported page is not auto-added to any menu — you decide where it lives. |
| Translations of the copied content | ⚠️ (planned: included when present) | Per-language override values are embedded alongside the base language when they exist on the source. |
Rows with computed PHP
A handful of design-library rows ship a PHP block (computed properties for dynamic data). Because the design library is shared via the repo, the matching template is present on every install. The template also anchors the paste-time construct gate: for non-admin editors, a pasted row may only carry executable constructs its own template (or the item-library catalogue) already declares — a row referencing a template the target lacks falls back to an empty baseline and refuses any executable construct.
Security: fetching attacker-influenceable URLs
Fetch-over-HTTPS means the target server downloads images from URLs that came in off the clipboard. That is a server-side request forgery (SSRF) surface, so the fetch is guarded:
- Scheme allowlist — only
http/httpsURLs are fetched. - Private-range block — the resolved IP must not be loopback, link-local, or RFC-1918 private (blocks
127.0.0.1,169.254.169.254cloud-metadata, internal services). - Content-type check — the response must be an
image/*type. - Size cap — oversized responses are rejected before they're written to disk.
Paste is an authenticated, role-gated editor action, so the attacker would already need editor access — but the guard is mandatory regardless, because the whole point of the feature is to fetch URLs the operator didn't author.
Using it
Copy a single row
- In the page editor, open a row's actions and choose Copy row (or select the row and press Ctrl/Cmd+C).
- The row's content + image manifest is written to your clipboard.
Copy a whole page
- Use Copy all sections in the editor toolbar.
- Every section's content travels as one
pagepayload.
Paste into another site
- Open the target site's editor in another tab (both sites must be HTTPS).
- Press Ctrl/Cmd+V outside any text field.
- A row payload appends the row(s) into the page you're editing.
- A page payload prompts Import as new page; confirm to create it.
- Referenced images download into the target's media library automatically; the row renders with its images in place.
Relationship to other features
- Backups & Page Revisions — the per-page download/upload bundle is the file-based sibling of this feature: same self-contained payload idea, but as a portable
.ziprather than the OS clipboard. Use the bundle for local → live moves (where the async clipboard isn't available) and for archiving; use copy/paste for the fast live → live flow. They share the same content-bundle primitive under the hood. - Shared Rows — shared rows propagate a single row across pages within one install. Cross-site copy/paste moves content between installs. Pasting a shared row produces an independent local copy on the target, not a live link back to the source.
- Media Library — fetched images become first-class media items on the target, with the same alt/caption/dimension handling as any upload.
- Draft Recovery — a paste is a normal editor mutation, so it participates in the dirty-state/undo flow and isn't committed until you save the page.
Where this lives in the code
- Envelope builder —
app/Support/ContentClipboardPayload.php(forRow/forRows/forPage; parses the server-owned row blades into the version-2 document trees). - Copy + clipboard write, paste detection — the store's
copyRowsToOsClipboard/pasteRowsFromClipboardOrLegacyinresources/js/editor/editor-state.js, plus the same-originrowsForClipboardlocalStorageshapes (webprocms_copied_single_row/webprocms_copied_rows_multi) consumed bypasteSingleRow()/pasteAllRows(). - Import side —
app/Support/ContentBundleImporter.php(doc compile + gate + overrides + media) with the doc-adoption funnel inapp/Support/RowDoc/RowDocPaste.phpand the SSRF guard inapp/Support/Media/SafeImageFetcher.php; the editor entry point ispasteContentBundle()inapp/Concerns/EditorClipboardActions.php. - File-based sibling ("Import as new page" from a
.zip) —app/Support/Pages/PageBundleService.php, reusing the same envelope + importer.