Quevin

Building a CKEditor 5 Plugin to Bridge Aprimo DAM and Drupal: A Technical Deep Dive

Kevin P. Davison
Drupal CKEditor Web Development Performance DAM AI Development Claude Code
Building a CKEditor 5 Plugin to Bridge Aprimo DAM and Drupal: A Technical Deep Dive

What Is Aprimo, and Why Does It Matter?

Aprimo is a cloud-based Digital Asset Management (DAM) platform that serves as the single source of truth for all of the company’s digital media. Every product photo, technical illustration, marketing banner, and document that appears on the website originates from Aprimo. It’s where our creative teams upload, tag, and manage renditions of assets — and where our web teams go to retrieve them.

At the company, we operate a dual-system Drupal 10 architecture. Our ECM system (Enterprise Content Management) is where content authors create and manage content. That content is indexed to an Apache Solr cluster, which our FRONT system (the public website) queries to render pages for millions of engineers, electricians, and maintenance professionals worldwide.

Aprimo sits alongside this pipeline. When a content author needs to attach an image to a product page or a document to a support article, they click an “Aprimo Browser” button on the content form, select the asset from Aprimo’s hosted interface, and the ECM creates a Drupal Media entity linked to that asset’s CDN URL. The asset never leaves Aprimo’s infrastructure — we reference it by URL, and Aprimo handles rendition generation, CDN delivery, and lifecycle management.

This integration has worked well for structured content fields — the dedicated image and document fields on our content types. But there was a gap: rich text fields. When authors needed to embed an Aprimo image inside a body field (a WYSIWYG editor), they had no path to the DAM. They were uploading images manually, bypassing Aprimo entirely, losing traceability and missing out on optimized renditions.

This is the story of how we closed that gap.

The Problem

Our CKEditor 5 toolbar had a standard “Insert Image” button that uploaded files directly to Drupal’s filesystem. These images were:

  • Not tracked — no connection to Aprimo, no asset ID, no audit trail
  • Not optimized — a single resolution uploaded at whatever size the author happened to have
  • Not responsive — no srcset, no responsive breakpoints, no consideration for mobile devices
  • Not consistent — different authors uploaded different sizes, formats, and quality levels

Meanwhile, our structured fields (product images, category pages) were pulling optimized, multi-rendition images from Aprimo with proper srcset attributes. The body field was the outlier.

Planning the Solution

Research Phase

Before writing any code, we needed to understand three things:

  1. How the existing Aprimo connector works. Our ecm_aprimo module uses a straightforward pattern: open Aprimo’s hosted content selector in a popup window, listen for a postMessage response containing the selected asset’s metadata and CDN URL, then create a Drupal Media entity. The key insight was that this flow is entirely client-side — no server-to-server API calls, no OAuth tokens. Just a popup and postMessage.

  2. How CKEditor 5 plugins work in Drupal 10. Drupal’s CKEditor 5 integration requires a specific structure: a .ckeditor5.yml file that registers the plugin and its toolbar button, a PHP class that can inject dynamic configuration, and a webpack-bundled JavaScript plugin that follows CKEditor 5’s plugin architecture (separate UI, Editing, and Command classes). We found a clean reference implementation in the token_filter contrib module.

  3. How the FRONT system handles responsive images. This was the critical discovery. Our FRONT system’s BuildTemplateService already knows how to take an Aprimo CDN URL and generate srcset attributes by swapping rendition suffixes on the URL. The pattern is deterministic: given any Aprimo asset UUID, you can construct URLs for every standard rendition by appending known suffixes like _7684_col (315px), _5256_col (360px), _0px12_col (480px), and original__size (990px).

The Key Architectural Decision

We had to choose how to insert assets into CKEditor:

Option A: Create a Drupal Media entity and insert a <drupal-media> embed tag. This is Drupal’s “proper” way to embed media in rich text. But it required enabling the media_embed filter (which we don’t use), it would introduce server-side rendering overhead for every inline asset, and critically — our FRONT system consumes body HTML directly from Solr. A <drupal-media> tag would arrive at the FRONT system as an unresolvable placeholder.

Option B: Insert raw <img> and <a> tags with Aprimo CDN URLs. Simpler, no new dependencies, and the HTML passes through Solr to the FRONT system exactly as authored. We added a data-aprimo-id attribute for traceability back to the source asset.

We chose Option B. The trade-off — inline Aprimo assets don’t appear in Drupal’s Media Library — was acceptable because Aprimo itself is our asset registry, and the data-aprimo-id attribute provides the audit trail.

Building It

Module Structure

We built the plugin as a sub-module of our existing ecm_aprimo module:

ecm_aprimo/modules/ecm_aprimo_ckeditor/

This keeps CKEditor concerns separate from the field widget integration and allows the toolbar button to be enabled or disabled independently.

The PHP Layer

A single PHP class, AprimoBrowser, extends CKEditor5PluginDefault and injects our Aprimo configuration (tenant URL, selector options) into the CKEditor 5 editor instance via getDynamicPluginConfig(). This means the JavaScript plugin doesn’t need to make separate configuration requests — it reads the Aprimo tenant URL directly from the editor config at runtime.

The JavaScript Layer

Five source files, following the standard CKEditor 5 plugin pattern:

  • aprimo-browser-ui.js — Registers the toolbar button with an icon and tooltip. When clicked, it executes the insertAprimoAsset command.

  • aprimo-browser-command.js — The core logic. Opens the Aprimo popup, attaches a one-time postMessage listener, determines whether the selected asset is an image or file based on extension, and inserts the appropriate HTML at the cursor position. For images from media.COMPANY.com, it builds the full srcset using the rendition suffix mapping.

  • aprimo-browser-editing.js — Extends CKEditor 5’s image model schema to allow custom attributes (data-aprimo-id, srcset, sizes, loading) and registers upcast/downcast converters so these attributes survive the editing roundtrip. Without these converters, CKEditor 5 would strip any attribute it doesn’t recognize.

  • aprimo-browser.js — The main plugin class that wires UI and Editing together.

  • index.js — The entry point for webpack.

The Srcset Builder

This was the most satisfying piece to implement. When an author selects a JPG or PNG from Aprimo, the command:

  1. Extracts the asset UUID from the URL using a regex
  2. Strips any existing rendition suffix
  3. Constructs four rendition URLs using the standard suffixes
  4. Builds the srcset and sizes attributes matching our FRONT system’s breakpoints

The result is an <img> tag that the browser can use to select the optimal image size:

<img src="https://media.COMPANY.com/{uuid}_original__size.jpg"
     srcset="...{uuid}__7684_col.jpg 315w,
             ...{uuid}__5256_col.jpg 360w,
             ...{uuid}__0px12_col.jpg 480w,
             ...{uuid}_original__size.jpg 990w"
     sizes="(min-width: 345px) and (max-width: 510px) 480w,
            (min-width: 511px) and (max-width: 752px) 315w,
            (min-width: 753px) 990w, 315w"
     loading="lazy"
     data-aprimo-id="asset-id"
     alt="Asset Title">

A visitor on a mobile device downloading a 315px-wide image instead of the full 990px original is a significant bandwidth savings — multiplied across every image on every page.

The Build Pipeline

We used webpack with CKEditor 5’s DLL reference plugin, matching the exact pattern from the token_filter contrib module. The CKEditor 5 version in our package.json must match what Drupal core ships (currently ~44.0.0). The built bundle is committed to the repository; node_modules is not.

The AI-Augmented Development Workflow

This plugin was built using an AI-augmented development workflow powered by Claude Code with Opus 4.6 as the primary coding agent and a separate document organizer agent running Sonnet 4.6 for structured documentation tasks. It’s a pattern I’ve been refining over the past year for enterprise Drupal work, and this project was a clean test of how it performs on a greenfield module with a well-defined scope.

The 20/60/20 Split

The workflow broke down into three phases:

20% Planning. Before opening Claude Code, I spent time defining the problem, researching the three integration surfaces (Aprimo connector, CKEditor 5 plugin architecture, FRONT system rendition patterns), and making the key architectural decision (Option B: raw <img> tags over <drupal-media> embeds). This phase also included gathering reference implementations — the token_filter module’s plugin structure, the BuildTemplateService rendition suffix map — and assembling them into a context document that Claude Code could consume. The planning phase is where the human sets the direction. If you skip it or under-invest, you spend the development phase correcting course instead of building.

60% Development interaction with Claude Code. With the architecture decided and the reference materials loaded as project context, Claude Code (Opus 4.6) handled the bulk of the implementation: scaffolding the module structure, writing the PHP plugin class, generating the five JavaScript source files following CKEditor 5’s plugin pattern, building the srcset logic from the rendition suffix map, and configuring the webpack build. My role during this phase was reviewing each output, testing it against the running Drupal environment, and steering corrections when the generated code didn’t account for our specific Drupal configuration (CKEditor 5 DLL compatibility, for example, required matching the exact version Drupal core ships). This isn’t “type a prompt and get a module.” It’s a continuous conversation where the developer provides domain expertise, validates against the real system, and the AI handles the mechanical translation of requirements into code.

20% Review, incremental commits, and documentation. The final phase was tightening: reviewing each file for correctness and maintainability, making incremental commits with descriptive messages as each component solidified, running a security review, and producing documentation.

We ran Claude Code’s /security-review against the changeset. It examined seven areas — postMessage handling, PHP config injection, XSS via inserted content, SQL/command injection, path traversal, authentication/authorization, and secrets/credentials — and found all clean except one note: the postMessage origin validation uses includes() instead of strict equality, which is weak by best practice but not concretely exploitable given DNS constraints, CKEditor’s link protocol sanitization, and Drupal’s text format filters (rated 2/10 confidence of exploitability). That’s a known trade-off we accepted during planning — Aprimo’s content selector can return messages from multiple subdomains, making strict origin matching impractical without maintaining a brittle allowlist.

This is where the Sonnet 4.6 document organizer agent came in — it processed the development conversation, my notes, and the final codebase into two documentation outputs:

  1. Comprehensive technical documentation — a detailed project context document covering architecture decisions, integration patterns, the rendition suffix mapping, and the CKEditor 5 plugin structure. This document will serve as project context for future Claude Code sessions working on this module, so the next developer (or the next AI session) starts with full knowledge of why decisions were made.

  2. A README.md in the sub-module root (ecm_aprimo/modules/ecm_aprimo_ckeditor/README.md) — covering installation, configuration, the toolbar button, supported asset types, and the srcset generation logic. Standard developer onboarding documentation, the kind that often gets skipped when a module is built under deadline pressure.

Why This Matters

The documentation point is worth emphasizing. In a traditional development workflow, documentation is the first thing cut when time gets tight. With an AI agent handling the structured writing — converting a development conversation into organized technical docs — the marginal cost of good documentation drops close to zero. The project context document means institutional knowledge doesn’t live only in the developer’s head. The README means the next person to touch this module doesn’t need to reverse-engineer it.

The 20/60/20 split also reflects something I’ve learned about AI-augmented development more broadly: the human’s value is concentrated at the edges. The planning phase (defining the problem correctly, choosing the right architecture, assembling the right context) and the review phase (validating against the real system, ensuring maintainability, committing clean history) are where experience matters most. The middle 60% is where AI excels — translating well-defined requirements into well-structured code, fast.

Lessons Learned

1. CKEditor 5’s Data Pipeline Is Strict — and That’s Good

Unlike CKEditor 4, which was relatively permissive with HTML attributes, CKEditor 5 will strip anything it doesn’t explicitly know about. Every custom attribute needs a model schema extension and upcast/downcast converters. This felt like overhead at first, but it means you can trust that what’s in the model is what’s in the HTML — no silent data loss.

2. Look for Existing Patterns Before Designing New Ones

The token_filter contrib module gave us a complete, working reference for the file structure, webpack config, PHP plugin class, and JavaScript architecture. The FRONT system’s BuildTemplateService gave us the exact rendition suffix mapping and sizes breakpoints. We didn’t invent anything — we connected existing patterns.

3. Respect the Architecture You Have

It was tempting to pursue the <drupal-media> embed approach because it’s Drupal’s canonical pattern. But our architecture — ECM indexes to Solr, FRONT reads from Solr — means the body HTML needs to be self-contained. An unresolvable embed tag would have been worse than no feature at all. Sometimes the “right” approach for the framework is the wrong approach for the system.

4. The Popup Pattern Is Resilient

Aprimo’s content selector uses a simple window.open() + postMessage pattern. No SDK, no npm package, no API versioning concerns. When Aprimo deprecated their REST APIs in March 2026, our integration was unaffected (we assessed this risk back in August 2025 and documented it). Web standards age well.

5. Traceability Matters More Than You Think

Adding data-aprimo-id to every inserted image was a small implementation detail, but it closes the loop between “which content uses this asset?” and the DAM. When an asset is updated in Aprimo, we can find every body field that references it.

6. AI-Augmented Development Is a Workflow, Not a Shortcut

Claude Code didn’t write this plugin while I went for coffee. It wrote code that I reviewed, tested, corrected, and committed. The speed gain is real — this would have taken significantly longer without it — but the quality comes from the human’s domain knowledge during planning and review. Running /security-review against the final changeset is a good example: the AI flagged the includes()-based origin check, but only a developer who understands Aprimo’s subdomain architecture can evaluate whether that’s an acceptable trade-off. The AI is strongest when the problem is well-defined and the context is rich. Invest in both.

How We’ll Use This Day-to-Day

Our WebOps team builds and maintains hundreds of content pages — product landing pages, technical articles, application notes, and support documentation. Before this plugin, embedding an optimized image in a body field required manually constructing <img> tags with srcset in CKEditor’s source editing mode. Most authors didn’t do this. They uploaded a single image and moved on.

Now, the workflow is:

  1. Click the Aprimo DAM button in the CKEditor toolbar
  2. Select the image from Aprimo’s familiar interface
  3. The plugin inserts a fully responsive <img> tag with four rendition sizes, proper sizes breakpoints, and loading="lazy"

No source editing. No manual URL construction. No uploading files outside of the DAM.

The Performance Impact

Every image inserted through this plugin is automatically optimized for:

  • Mobile devices — A phone on a cellular connection downloads the 315px rendition instead of the 990px original. For a typical product image, that’s the difference between ~40KB and ~200KB.
  • Lazy loading — Images below the fold don’t load until the user scrolls near them, reducing initial page weight.
  • Core Web Vitals — Properly sized images directly improve Largest Contentful Paint (LCP) scores. Lazy loading reduces Total Blocking Time (TBT) by deferring off-screen image decoding.

These aren’t theoretical improvements. Our structured content fields (product images, category pages) already use this exact rendition and srcset pattern, and we’ve measured the difference. Extending it to body fields closes the last gap where unoptimized images were entering the page.

Looking Ahead

The rendition mapping is currently hardcoded to four standard breakpoints. As we evolve our front-end architecture, we can update the suffix map in a single location (aprimo-browser-command.js) and rebuild. If Aprimo adds new rendition presets, we add a line to the array.

The plugin is also structured to be extensible. The same pattern — popup, postMessage, insert at cursor — could be adapted for other DAM providers or internal asset libraries. The CKEditor 5 plugin architecture makes it straightforward to add new commands without touching the existing toolbar button registration.

Summary

What started as a gap in our content authoring workflow — “I can’t insert an Aprimo image into a body field” — became an opportunity to improve page performance across the site. By connecting Aprimo’s hosted content selector to CKEditor 5’s plugin system and replicating our FRONT system’s responsive image patterns, we gave our WebOps team a one-click path to optimized, responsive, lazy-loaded images that are tracked back to the DAM.

The total implementation: one PHP class, five JavaScript source files, a webpack build, and a handful of YAML — built through an AI-augmented workflow using Claude Code with Opus 4.6 for development and Sonnet 4.6 for documentation, with comprehensive technical docs and a README committed alongside the code. Sometimes the best solutions are the ones that connect existing patterns rather than inventing new ones — and sometimes the best way to connect them is to invest your time in planning and review while an AI handles the translation in between.

Kevin P. Davison

About the Author

Kevin P. Davison has over 20 years of experience building websites and figuring out how to make large-scale web projects actually work. He writes about technology, AI, leadership lessons learned the hard way, and whatever else catches his attention—travel stories, weekend adventures in the Pacific Northwest like snorkeling in Puget Sound, or the occasional rabbit hole he couldn't resist.