In a few months, my personal blog will turn 13 years old. Over that time, I’ve published hundreds of articles. For most of it, I followed a simple rule: every article should include a custom image.
At first, that meant manually sourcing visuals. Later, creating them myself. Today, with AI tools, generating images is trivial.
But that leads to a more interesting question: do we actually need a separate image for every article?
I’ve written dozens of posts about custom images in Drupal views. Back in the day, I was particularly eager to follow Drupal’s best practices around media and presentation.
Why images mattered
For me, the answer was never purely visual. It was about distribution: Facebook Open Graph previews, Twitter/X cards, Link sharing
Without an image, posts looked incomplete. With one, they looked credible. That said, social media was never my strongest channel. Most of my traffic historically came from Google SEO. And then… that changed.
From SEO to AI
Search traffic is no longer what it used to be. We’re now in a phase where: AI summarizes content, Users rarely click through, Distribution is fragmented
A few days ago, I asked ChatGPT to evaluate my blog. It got… ambitious. It essentially reframed my personal blog into something resembling an institutional hedge fund publication, suggested Substack subscriptions, and pushed toward a more structured content strategy. Overkill? Maybe. But one idea stuck.
What I really like about Substack is this: It automatically extracts an image from the article content and uses it everywhere - headers, previews, social sharing
No duplication. No extra fields. No friction. That made me question my Drupal setup.
The Drupal problem
In Drupal 10/11, the typical setup looks like this:
bodyfield → content with inline imagesfield_images(or similar) → used for:Views listings
Featured images
Open Graph
That means:
Insert image inline
Upload/select same image again in another field
Or forget step #2 and break your previews. This duplication has been around for years.
Instead of forcing editors to do extra work: Let Drupal extract the first inline image and reuse it automatically. Exactly like Substack.
The solution: a tiny custom module
With a few prompts, ChatGPT helped generate a working Drupal module that:
Hooks into node save
Parses the body HTML
Finds the first inline image
Copies it into
field_images
No UI changes. No training. No workflow disruption.
Module structure
Create:
1. blog_first_image_sync.info.yml
type: module
description: 'Copies the first inline body image into field_images automatically.'
package: Custom
core_version_requirement: ^10 || ^11
dependencies:
- drupal:node
- drupal:file
- drupal:image
2. blog_first_image_sync.module
declare(strict_types=1);
use Drupal\node\NodeInterface;
/**
* Implements hook_node_presave().
*/
function blog_first_image_sync_node_presave(NodeInterface $node): void {
// Only act on nodes that have both required fields.
if (!$node->hasField('body') || !$node->hasField('field_images')) {
return;
}
// Skip empty body.
if ($node->get('body')->isEmpty()) {
return;
}
$html = (string) $node->get('body')->value;
if (trim($html) === '') {
return;
}
// Parse HTML safely.
libxml_use_internal_errors(TRUE);
$dom = new \DOMDocument();
$loaded = $dom->loadHTML(
'<?xml encoding="utf-8" ?><html><body>' . $html . '</body></html>'
);
libxml_clear_errors();
if (!$loaded) {
return;
}
$xpath = new \DOMXPath($dom);
// Find first inline image with Drupal file reference.
$nodes = $xpath->query('//img[@data-entity-type="file" and @data-entity-uuid]');
if (!$nodes || $nodes->length === 0) {
return;
}
$first_image = $nodes->item(0);
$uuid = $first_image->getAttribute('data-entity-uuid');
if ($uuid === '') {
return;
}
// Load file entity from UUID.
$file = \Drupal::service('entity.repository')
->loadEntityByUuid('file', $uuid);
if (!$file) {
return;
}
// Extract optional attributes.
$alt = $first_image->getAttribute('alt') ?: '';
$title = $first_image->getAttribute('title') ?: '';
// Set image field (overwrites existing value).
$node->set('field_images', [
[
'target_id' => $file->id(),
'alt' => $alt,
'title' => $title,
],
]);
}
Installation
mkdir blog_first_image_sync
# add files
drush en blog_first_image_sync -y
drush cr
How it works
On every node save (create or update):
Drupal reads the body HTML
Finds the first inline image
Extracts its
data-entity-uuidLoads the corresponding file entity
Writes it into
field_images
Your Views and Open Graph logic continue working unchanged.
Why this works well in Drupal 10/11
Modern Drupal stores inline images like: <img data-entity-uuid="..." data-entity-type="file">. That means:
No guessing file paths
No brittle parsing
Direct mapping to Drupal entities
This is key. Without it, the solution would be much messier.
Result
After enabling:
Insert image inline → done
Save → auto-populated
Views → unchanged
Social previews → consistent
No duplication.
The most interesting part wasn’t the code.It was this - with a few prompts, AI helped design and implement a production-ready Drupal feature. Not perfect on first try (wrong content type, small bugs), but fast enough to iterate in minutes instead of hours.
Final thoughts
Drupal remains extremely flexible
Many long-standing UX issues are solvable with small custom modules
AI significantly reduces the cost of building those solutions
It’s been years since I stopped active Drupal development—these days I only use it for my own projects. That said, if you feel you could benefit from some guidance, feel free to reach out to arrange a paid one-on-one coaching session.