Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

🔗 Deep Dive into Entity Link Targets in Drupal 11.3

By admin, 21 November, 2025

How Drupal Finally Solved URL Generation for CKEditor 5

Drupal 11.3 introduces a major improvement to how URLs are generated for internal entities, especially when working inside CKEditor 5. The new Entity Link Target system allows entity types to declare exactly how their hyperlink should be generated — whether they represent content, files, media, or even virtual objects like menu links.

This article goes far beyond the official change record, providing a full technical deep dive with flow diagrams, architectural notes, and production-ready examples.


🧭 Why Drupal Needed Link Target Handlers

Before Drupal 11.3:

  • CKEditor 5 had no unified way to link to an entity.
  • Each entity type had its own approach to generating URLs.
  • Linking a Media or File entity often produced the wrong URL (e.g., linking to the media page instead of the underlying file).
  • Developers duplicated “how to compute the right link” code in different modules.
  • Some Drupal entities do not even have canonical routes (e.g., MenuLinkContent).

The result

❌ Broken links
❌ Wrong links
❌ Missing cacheability metadata
❌ Missing link normalization
❌ Inconsistent user experience

Drupal 11.3 introduces a new system that fixes all of this cleanly.


🧩 What Is a Link Target Handler?

A link target handler is a class that tells Drupal:

“When someone links to this entity, this is the correct URL to use.”

Entity type definitions can include a new optional section:

"link_target" = {
  "view" = "\Drupal\media\Entity\MediaLinkTargetStandaloneWhenAvailable",
  "download" = "\Drupal\media\Entity\MediaLinkTarget",
},

Each handler must implement:

EntityLinkTargetInterface

public function getLinkTarget(EntityInterface $entity): GeneratedUrl;

The returned GeneratedUrl object contains:

  • The final URL as a string
  • Cacheability metadata (contexts, tags, max-age)
  • No route or path ambiguity

This makes CKEditor 5 much smarter when inserting links.


🧱 Architecture Overview

Here is how the entire flow works when an editor inserts a link:

CKEditor 5 UI
     |
     v
Drupal editor plugin
     |
     v
Entity Reference selection dialog
     |
     v
EntityLinkTargetResolver
     |
     v
EntityType -> handlers[link_target]
     |
     v
Selected handler (view|download|custom)
     |
     v
getLinkTarget() returns GeneratedUrl
     |
     v
CKEditor inserts proper URL <a href="...">

Every entity type can now independently define:

  • what “view link” means
  • what “download link” means
  • what “embed link” could mean (future extension)

📘 Core Example #1: Files (FileLinkTarget)

Files should always link to the physical file URL.
Core’s implementation:

class FileLinkTarget implements EntityLinkTargetInterface {

  public function getLinkTarget(EntityInterface $entity): GeneratedUrl {
    assert($entity instanceof FileInterface);

    $url = $entity->createFileUrl(TRUE);

    return (new GeneratedUrl())
      ->setGeneratedUrl($url)
      ->setCacheMaxAge(Cache::PERMANENT);
  }
}

Why this matters

  • CKEditor always links directly to /sites/default/files/...
  • File aliasing and private files are handled automatically
  • No route lookup — permanent caching is possible

📗 Core Example #2: MenuLinkContent

Menu link content entities do not have a canonical entity route.
Their destination is the router link they represent.

class MenuLinkContentLinkTarget implements EntityLinkTargetInterface {

  public function getLinkTarget(EntityInterface $entity): GeneratedUrl {
    assert($entity instanceof MenuLinkContentInterface);

    return $entity->getUrlObject()->toString(TRUE);
  }
}

Why this matters

It enables linking to menu-defined routes even when:

  • They map to controllers
  • They have dynamic parameters
  • They point to external URLs

🎯 Core Example #3: Media Entities

Media can represent:

  • A file → file URL
  • An OEmbed → YouTube/Vimeo URL
  • A remote document → external link
  • A custom media source → arbitrary URL

This was previously impossible to normalize consistently.

In Drupal 11.3, Media defines two handlers:

"view"     → standalone view URL
"download" → file URL (if applicable)

🛠️ Building a Custom Link Target Handler

Let’s build a real example from scratch.

Scenario

You have a custom entity type Product.
You want CKEditor to insert links like:

/shop/{product_id}

regardless of how the entity itself is routed.


Step 1 — Add link_target to your entity type

my_module.my_product.yml

my_product:
  label: 'Product'
  class: Drupal\my_module\Entity\Product
  handlers:
    storage: Drupal\my_module\ProductStorage
    link_target:
      view: Drupal\my_module\LinkTarget\ProductLinkTarget

Step 2 — Implement the handler

src/LinkTarget/ProductLinkTarget.php

namespace Drupal\my_module\LinkTarget;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityLinkTargetInterface;
use Drupal\Core\GeneratedUrl;

class ProductLinkTarget implements EntityLinkTargetInterface {

  public function getLinkTarget(EntityInterface $entity): GeneratedUrl {
    // Build the canonical URL (or a custom route).
    $url_object = $entity->toUrl('canonical');

    $generated = $url_object->toString(TRUE);

    $url = new GeneratedUrl();
    $url->setGeneratedUrl($generated->getGeneratedUrl());
    $url->addCacheableDependency($generated);

    return $url;
  }
}

🎨 Optional Enhancement: Custom Shop Route

Maybe your “canonical” route is not the customer-facing one.
You can instead use a custom front-end route:

$route = Url::fromRoute('my_module.product_display', [
  'product' => $entity->id(),
]);

$generated = $route->toString(TRUE);

Now CKEditor inserts URLs like:

/shop/product/42

🧪 Testing the Link Target

Use Drupal’s core service:

$link_target_manager = \Drupal::service('entity.link_target_resolver');
$generated = $link_target_manager->getLinkTarget($product, 'view');

🧠 Advanced Topic: Multiple Link Target Types

Drupal supports multiple target types:

  • view
  • download
  • custom types (e.g., embed, preview, short_url)

Example:

"link_target" = {
  "view" = "\Drupal\my_module\LinkTarget\ProductViewTarget",
  "embed" = "\Drupal\my_module\LinkTarget\ProductEmbedTarget",
}

CKEditor plugins can choose which one to use.

This opens the door for:

  • Embeddable product cards
  • Download-only links
  • External integration handlers
  • Headless/decoupled link generation

🔍 Debugging Tips

View active handlers

\Drupal::entityTypeManager()->getDefinition('media')->getHandlerClasses('link_target');

Dump the generated URL

dump($generated->getGeneratedUrl());

Check cacheability

dump($generated->getCacheTags());
dump($generated->getCacheContexts());

🏁 Summary

The new Entity Link Target system in Drupal 11.3 is a foundational improvement that:

  • Provides unified URL generation for all entity types
  • Makes CKEditor linking reliable and extensible
  • Handles cache metadata correctly
  • Eliminates duplicated code in modules
  • Enables complex linking logic (file vs. media vs. menu vs. custom entities)

For developers, this is one of the most important backend improvements in Drupal 11 — small in appearance, but huge in impact.

Tags

  • Drupal
  • CKEditor 5
  • Entity Link Target

Comments

About text formats

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Powered by Drupal