
Research
Shai-Hulud Descends to Hades: Miasma Worm Campaign Spreads with New PyPI Wave
Socket found 37 malicious PyPI wheels that abuse Python startup hooks to launch a Bun-powered credential stealer tied to Mini Shai-Hulud/Miasma.
@datocms/content-link
Advanced tools
Lightweight library for DatoCMS visual editing overlays and content links.
Click-to-edit overlays for DatoCMS projects. Platform and framework agnostic, two function calls to set it up.
npm install @datocms/content-link
Make sure you pass the contentLink and baseEditingUrl options when initializing the DatoCMS CDA client:
import { executeQuery } from "@datocms/cda-client";
const result = await executeQuery(query, {
token: process.env.DATO_API_TOKEN,
contentLink: 'v1',
baseEditingUrl: 'https://acme.admin.datocms.com', // <- URL of your DatoCMS project (https://<YOUR-PROJECT-NAME>.admin.datocms.com)
});
import { createController } from '@datocms/content-link';
const controller = createController();
controller.enableClickToEdit();
Note: You can also skip calling enableClickToEdit() and temporarily enable click-to-edit mode on-demand by holding down the Alt/Option key. The mode will be active while the key is held and automatically disable when released.
That's all you need for the majority of projects! If you see overlays and deep links opening the correct records, your setup is complete!
createController(options?)import { createController } from '@datocms/content-link';
// Minimal (no options required)
const controller = createController();
// With options
const controller = createController({
// Optional: limit scanning/observation to this root instead of the whole document.
// Can be a ShadowRoot or a specific container element.
root: document.getElementById('preview-container'),
// Optional: strip stega-encoded invisible characters from text content (default: false)
stripStega: false,
// Optional: hue (0–359) of the overlay accent color (default: 17, orange)
hue: 200
});
// Control click-to-edit overlays
controller.enableClickToEdit(); // turn click-to-edit overlays on
controller.enableClickToEdit({ // with visual flash highlighting all editable elements
scrollToNearestTarget: true // optionally scroll to nearest editable if none visible
});
controller.disableClickToEdit(); // turn click-to-edit overlays off
controller.isClickToEditEnabled(); // check if click-to-edit is currently enabled
controller.isDisposed(); // check if disposed
controller.dispose(); // permanently tear down and clean up (controller becomes inert)
Returns a controller to manage DOM stamping and click-to-edit overlays.
Options:
root?: ParentNode: Limit scanning to a specific container (default: document)hue?: number: Hue angle (0–359) of the overlay accent color (default: 17, orange). The library automatically computes a lightness value that guarantees readable white text on the overlay label at any hue.stripStega?: boolean: Whether to strip stega-encoded invisible characters from text content after stamping (default: false). Stega embeds invisible, zero-width UTF-8 characters into text content to encode editing metadata.
false (default): Stega encoding remains in the DOM, allowing controllers to be disposed and recreated on the same page. The invisible characters don't affect display but preserve the source of truth.true: Stega encoding is permanently removed from text nodes, providing clean textContent for programmatic access. However, recreating a controller on the same page won't detect elements since the encoding is lost.Controller methods:
enableClickToEdit(flashAll?: { scrollToNearestTarget: boolean }): Turn click-to-edit overlays on (allows clicking elements to open the editor). Optionally pass flashAll to briefly highlight all editable elements with an animated effect, and scroll to the nearest one if none are visible.disableClickToEdit(): Turn click-to-edit overlays off (DOM stamping continues)isClickToEditEnabled(): Returns true if click-to-edit is currently enabledisDisposed(): Returns true if the controller has been disposeddispose(): Permanently disconnects observers and cleans up. After dispose, the controller cannot be re-enabled; create a new one if neededflashAll(scrollToNearestTarget?: boolean): Briefly highlight all editable elements with an animated effect. Optionally scroll to the nearest editable element if none are visible.Keyboard shortcuts:
Note: DOM stamping (detecting and marking editable elements) runs automatically when the controller is created and continues until dispose() is called. Click-to-edit overlays are independent and must be explicitly enabled with enableClickToEdit().
When your website runs inside the Visual Editing mode of the Web Previews plugin, the controller automatically establishes bidirectional communication with the plugin.
This connection is completely automatic and requires no configuration. If your preview is not running in an iframe or the connection fails, the library gracefully falls back to opening edit URLs in a new tab.
If your website uses client-side routing (like Next.js, React Router, etc.), you need to set up bidirectional communication with the plugin:
// Next.js App Router example
'use client';
import { createController } from '@datocms/content-link';
import { useRouter, usePathname } from 'next/navigation';
import { useEffect } from 'react';
export default function PreviewPage() {
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
const controller = createController({
// Handle navigation requests from the plugin
onNavigateTo: (path) => {
router.push(path);
}
});
return () => controller.dispose();
}, [router]);
useEffect(() => {
// Notify the plugin when the URL changes
controller?.setCurrentPath(pathname);
}, [pathname]);
return <YourPageContent />;
}
Available option:
onNavigateTo?: (path: string) => void: Callback invoked when the Web Previews plugin requests navigation to a different URLAvailable method:
setCurrentPath(path: string): Notify the Web Previews plugin of the current URLBy default, the controller preserves stega-encoded invisible characters in the DOM. This allows you to safely dispose and recreate controllers on the same page without losing the ability to detect editable elements:
// Create initial controller
const controller1 = createController();
controller1.enableClickToEdit();
// Later, dispose it
controller1.dispose();
// Create a new controller - it will still find all editable elements
const controller2 = createController();
controller2.enableClickToEdit();
This is particularly useful for:
If you need clean text content for programmatic access (without invisible stega characters), use stripStega: true. However, note that this permanently removes the stega encoding, preventing controller recreation:
const controller = createController({ stripStega: true });
// After disposal, creating a new controller won't find elements
controller.dispose();
const controller2 = createController(); // Won't detect editable elements
You can show users where all the editable elements are on the page in two ways:
1. When enabling click-to-edit mode:
controller.enableClickToEdit({
scrollToNearestTarget: true
});
2. As a standalone method:
// Highlight all editable elements
controller.flashAll();
// Highlight and scroll to nearest editable if none visible
controller.flashAll(true);
This will:
scrollToNearestTarget is true and no editable elements are currently visible in the viewport, automatically scroll to the nearest editable elementThis is particularly useful for:
This library uses several data-datocms-* attributes. Some are developer-specified (you add them to your markup), and some are library-managed (added automatically during DOM stamping). Here's a complete reference.
These attributes are added by you in your templates/components to control how editable regions behave.
data-datocms-content-link-urlManually marks an element as editable with an explicit edit URL. Use this for non-text fields (booleans, numbers, dates, JSON) that cannot contain stega encoding. The recommended approach is to use the _editingUrl field available on all records:
query {
product {
id
price
isActive
_editingUrl
}
}
<span data-datocms-content-link-url={product._editingUrl}>
${product.price}
</span>
data-datocms-content-link-sourceAttaches stega-encoded metadata without the need to render it as content. Useful for structural elements that cannot contain text (like <video>, <audio>, <iframe>, etc.) or when stega encoding in visible text would be problematic:
<div data-datocms-content-link-source={video.alt}>
<video src={video.url} poster={video.posterImage.url} controls />
</div>
The value must be a stega-encoded string (any text field from the API will work). The library decodes the stega metadata from the attribute value and makes the element clickable to edit.
data-datocms-content-link-groupExpands the clickable area to a parent element. When the library encounters stega-encoded content, by default it makes the immediate parent of the text node clickable to edit. Adding this attribute to an ancestor makes that ancestor the clickable target instead:
<article data-datocms-content-link-group>
<h2>Title with stega</h2>
<p>Description with no stega</p>
</article>
Here, clicking anywhere in the <article> opens the editor, rather than requiring users to click precisely on the <h2>.
Important: A group should contain only one stega-encoded source. If multiple stega strings resolve to the same group, the library logs a collision warning and only the last URL wins.
data-datocms-content-link-boundaryStops the upward DOM traversal that looks for a data-datocms-content-link-group, making the element where stega was found the clickable target instead. This creates an independent editable region that won't merge into a parent group (see How group and boundary resolution works below for details):
<div data-datocms-content-link-group>
<h1>Title with stega (URL A)</h1>
<section data-datocms-content-link-boundary>
<span>Text with stega (URL B)</span>
</section>
</div>
Without the boundary, clicking "Text with stega" would open URL A (the outer group). With the boundary, the <span> becomes the clickable target opening URL B.
The boundary can also be placed directly on the element that contains the stega text:
<div data-datocms-content-link-group>
<h1>Title with stega (URL A)</h1>
<span data-datocms-content-link-boundary>Text with stega (URL B)</span>
</div>
Here, the <span> has the boundary and directly contains the stega text, so the <span> itself becomes the clickable target (since the starting element and the boundary element are the same).
These attributes are added automatically by the library during DOM stamping. You do not need to add them yourself, but you can target them in CSS or JavaScript.
data-datocms-contains-stegaAdded to elements whose text content contains stega-encoded invisible characters. This attribute is only present when stripStega is false (the default), since with stripStega: true the characters are removed entirely. Useful for CSS workarounds — the zero-width characters can sometimes cause unexpected letter-spacing or text overflow:
[data-datocms-contains-stega] {
letter-spacing: 0 !important;
}
data-datocms-auto-content-link-urlAdded automatically to elements that the library has identified as editable targets (through stega decoding and group/boundary resolution). Contains the resolved edit URL.
This is the automatic counterpart to the developer-specified data-datocms-content-link-url. The library adds data-datocms-auto-content-link-url wherever it can extract an edit URL from stega encoding, while data-datocms-content-link-url is needed for non-text fields (booleans, numbers, dates, etc.) where stega encoding cannot be embedded. Both attributes are used by the click-to-edit overlay system to determine which elements are clickable and where they link to.
When the library encounters stega-encoded content inside an element, it walks up the DOM tree from that element:
data-datocms-content-link-group, it stops and stamps that element as the clickable target.data-datocms-content-link-boundary, it stops and stamps the starting element as the clickable target — further traversal is prevented.Here are some concrete examples to illustrate:
Example 1: Nested groups
<div data-datocms-content-link-group>
<h1>Title with stega (URL A)</h1>
<div data-datocms-content-link-group>
<p>Paragraph with stega (URL B)</p>
</div>
</div>
<h1>, finds the outer group → the outer <div> becomes clickable (opens URL A).<p>, finds the inner group first → the inner <div> becomes clickable (opens URL B). The outer group is never reached.Each nested group creates an independent clickable region. The innermost group always wins for its own content.
Example 2: Boundary preventing group propagation
<div data-datocms-content-link-group>
<h1>Title with stega (URL A)</h1>
<section data-datocms-content-link-boundary>
<span>Text with stega (URL B)</span>
</section>
</div>
<h1>, finds the outer group → the outer <div> becomes clickable (opens URL A).<span>, hits the <section> boundary → traversal stops, the <span> itself becomes clickable (opens URL B). The outer group is not reached.Example 3: Boundary inside a group
<div data-datocms-content-link-group>
<p>Main content with stega (URL A)</p>
<div data-datocms-content-link-boundary>
<p>Isolated content with stega (URL B)</p>
</div>
</div>
<p>, finds the outer group → the outer <div> becomes clickable (opens URL A).<p>, hits the boundary → traversal stops, the <p> itself becomes clickable (opens URL B). The outer group is not reached.Example 4: Multiple stega strings without groups (collision warning)
<p>
Text with stega (URL A)
More text with stega (URL B)
</p>
Both stega-encoded strings resolve to the same <p> element. The library logs a console warning and the last URL wins. To fix this, wrap each piece of content in its own element:
<p>
<span>Text with stega (URL A)</span>
<span>More text with stega (URL B)</span>
</p>
Structured Text fields require special attention because of how stega encoding works within them:
<span> within the structured text output. Without any configuration, only that small span would be clickable.Here are the rules to follow:
This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:
<div data-datocms-content-link-group>
<StructuredText data={page.content} />
</div>
Embedded blocks and inline records have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Add data-datocms-content-link-boundary to prevent them from merging into the parent group:
<div data-datocms-content-link-group>
<StructuredText
data={page.content}
renderBlock={(block) => (
<div data-datocms-content-link-boundary>
<BlockComponent block={block} />
</div>
)}
renderInlineRecord={(record) => (
<span data-datocms-content-link-boundary>
<InlineRecordComponent record={record} />
</span>
)}
/>
</div>
With this setup:
import { decodeStega, stripStega, revealStega } from '@datocms/content-link';
// Decode a raw string that may contain stega
const info = decodeStega(someString);
// Returns: { origin: string, href: string } | null
// Remove stega characters for display
const clean = stripStega(someString);
// Make stega visible for debugging (works with any value, including full GraphQL responses)
const debug = revealStega(graphqlResponse);
console.log(JSON.stringify(debug, null, 2));
decodeStega(input: string)
{ origin: string, href: string } if stega is found, null otherwisestripStega(input: any)
VERCEL_STEGA_REGEX, then parses back to original typerevealStega(input: any)
stripStega[STEGA:/editor/item_types/…] marker// Works with strings
stripStega("Hello\u200EWorld") // "HelloWorld"
// Works with objects
stripStega({ name: "John\u200E", age: 30 })
// Works with nested structures - removes ALL stega encodings
stripStega({
users: [
{ name: "Alice\u200E", email: "alice\u200E.com" },
{ name: "Bob\u200E", email: "bob\u200E.co" }
]
})
// Works with arrays
stripStega(["First\u200E", "Second\u200E", "Third\u200E"])
// Reveal stega in a full GraphQL response for debugging
revealStega({
blog: {
title: "Hello World\u200E",
author: { name: "Alice\u200E" }
}
})
// {
// blog: {
// title: "Hello World[STEGA:/editor/item_types/123/items/456]",
// author: { name: "Alice[STEGA:/editor/item_types/789/items/012]" }
// }
// }
contentLink and baseEditingUrl options. baseEditingUrl should be set to your DatoCMS project admin URL (e.g., https://<YOUR-PROJECT-NAME>.admin.datocms.com). The stega-encoded metadata is only included in responses when these options are present. Also, make sure you've called enableClickToEdit() on the controller.enableClickToEdit().stripStega: false (the default). If you previously used stripStega: true, the stega encoding was permanently removed and cannot be recovered. In this case, you'll need to reload the page or re-fetch the content.stripStega: true, or use CSS: [data-datocms-contains-stega] { letter-spacing: 0 !important; }. This attribute is automatically added to elements with stega-encoded content when stripStega: false (the default).MIT © DatoCMS
FAQs
Lightweight library for DatoCMS visual editing overlays and content links.
We found that @datocms/content-link demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 7 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Socket found 37 malicious PyPI wheels that abuse Python startup hooks to launch a Bun-powered credential stealer tied to Mini Shai-Hulud/Miasma.

Security News
RubyGems and Bundler 4.0.13 introduced an opt-in cooldown feature that delays newly published gems during dependency resolution.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.