Author: Maxim Semenov
Website: smnv.org
Email: maxim@smnv.org

If this project helps your work, consider supporting future development: GitHub Sponsors or smnv.org/sponsor.
License: MIT
ProcessWire: 3.0+
Changelog: CHANGELOG.md
Dynamic content embed management system with live preview, custom PHP templates, and visual card builder for ProcessWire CMS.
((embed-name)) tags in any text field/site/modules/Embedr/:
/site/modules/Embedr/
βββ Embedr.module.php
βββ ProcessEmbedr.module
βββ TextformatterEmbedr.module
βββ EmbedrItem.php
βββ Embedrs.php
βββ EmbedrType.php
βββ EmbedrTypes.php
βββ EmbedrRenderer.php
Modules β Refresh β Embedr β Install
Setup β Fields β body (or your field)
Details β Textformatters β β Embedr Text Formatter
Setup β Embedr β Types β Add New
Name: articles
Template: articles.php (optional)
Setup β Embedr β Add New
Name: latest-articles
Title: Latest Articles
Type: articles
Selector: template=article, limit=6
Body field: ((latest-articles))
Embedr Ecosystem
βββ Embedr (Core API + Configuration)
βββ ProcessEmbedr (Admin Interface)
βββ TextformatterEmbedr (Parser)
βββ EmbedrItem (Single Embed Object)
βββ Embedrs (Embed Collection + Database)
βββ EmbedrType (Single Type Object)
βββ EmbedrTypes (Type Collection + Database)
βββ EmbedrRenderer (Visual Card Builder)
1. Text Field with ((embed-name))
β
2. TextformatterEmbedr (Textformatter)
β
3. Embedr::getEmbed('embed-name')
β
4. EmbedrItem::render()
β
5. Check: PHP Template exists?
βββ YES: Include custom PHP template
βββ NO: Use EmbedrRenderer (visual cards)
β
6. Return HTML
Access via: Setup β Modules β Embedr β Configure
Components Path (default: components/)
/site/templates/components/ β /site/templates/components/Opening Tag (default: (()
{{, [[)Closing Tag (default: )))
Auto-discover Types (default: unchecked)
.php files in components pathShow Type Icons (default: checked)
Debug Mode (default: unchecked)
embedr-debug logTypes define reusable configurations for similar embeds.
Example: Article List Type
Name: articles
Title: Article Lists
Icon: file-text
Template: articles.php β optional; leave blank to use the visual renderer
Mode: array
Embeds are instances of types with specific selectors.
Example: Latest Articles Embed
Name: latest-articles
Title: Latest Articles
Type: articles
Selector: template=article, sort=-created, limit=6
In any text field:
<h2>Recent Posts</h2>
((latest-articles))
<h2>Featured Products</h2>
((featured-products))
In PHP templates:
echo $page->body; // Textformatter processes ((tags)) automatically
Location: /site/templates/components/your-template.php
Available Variables:
$items // PageArray - Found pages from selector
$page // Page - Current page
$config // Config - ProcessWire config
$input // WireInput - Request data
$sanitizer // Sanitizer - Sanitization methods
$embed // EmbedrItem - The embed object
$embedContext // array - Optional context passed to Embedr::render()
<?php namespace ProcessWire;
/**
* Articles List Template
*/
if(!$items->count()) {
echo "<!-- No articles found -->";
return;
}
?>
<div class="article-grid">
<?php foreach($items as $article): ?>
<article class="card">
<?php if($article->images->count()): ?>
<img src="<?= $article->images->first()->width(400)->url ?>"
alt="<?= $article->title ?>">
<?php endif; ?>
<h3><?= $article->title ?></h3>
<?php if($article->summary): ?>
<p><?= $article->summary ?></p>
<?php endif; ?>
<a href="<?= $article->url ?>">Read more</a>
</article>
<?php endforeach; ?>
</div>
Always check field existence for guest users:
<?php namespace ProcessWire;
if(!$items->count()) return;
// Get current user
$user = $this->wire('user');
$isGuest = $user->isGuest();
?>
<div class="articles">
<?php foreach($items as $article): ?>
<article>
<?php
// Safe image access
if($article->hasField('images') && $article->images && $article->images->count()):
$img = $article->images->first();
if($img):
?>
<img src="<?= $img->width(400)->url ?>" alt="<?= $article->title ?>">
<?php
endif;
endif;
?>
<h3><?= $article->title ?></h3>
<?php
// Safe summary access
if($article->hasField('summary') && $article->summary):
?>
<p><?= $article->summary ?></p>
<?php endif; ?>
<a href="<?= $article->url ?>">Read more</a>
</article>
<?php endforeach; ?>
</div>
When no PHP template is specified, Embedr uses the built-in visual renderer powered by UIKit CSS classes.
| Setting | Default | Options |
|---|---|---|
| Layout | grid |
grid, list, table |
| Columns | 6 |
2, 3, 4, 6 |
| Image size | 192Γ192px |
configured per type via config JSON |
| Links | enabled | β |
The grid is fully responsive: 2 columns on small screens, scaling up to the configured maximum on large screens.
For custom image sizes, markup, or styling β create a PHP template instead (see Custom PHP Templates). The visual renderer is intended as a zero-config fallback.
embedr - View embeds
embedr-edit - Edit embeds
Access β Roles β [Role Name]
Permissions β β embedr
Permissions β β embedr-edit (if needed)
Setup β Modules β Embedr β Configure
β Debug Mode
Save
embedr-debug - Full execution log
Setup β Logs β embedr-debug
Shows:
embedr-errors - Error log only
Setup β Logs β embedr-errors
Shows:
[TextformatterEmbedr::formatValue] Called | Page=/news/hello-world/, User=guest
[TextformatterEmbedr::formatValue] Found 2 embed(s): latest-articles, featured
[TextformatterEmbedr::getReplacement] Looking for embed: latest-articles
[TextformatterEmbedr::getReplacement] Embed found | ID=3, Name=latest-articles, Type=articles
[Embedr::render] Starting | Embed=latest-articles (ID=3), Selector=template=53 | User=guest (guest=YES)
[Embedr::render] Type loaded | Name=articles, Template=articles.php, Mode=array
[Embedr::render] Executing selector: template=53
[Embedr::render] Selector found 6 items
[Embedr::render] Using PHP template: /site/templates/components/articles.php | Exists: YES
[Embedr::render] PHP template rendered (5089 chars)
[TextformatterEmbedr::getReplacement] Rendered (5089 chars): ...
<!-- Embedr: 'embed-name' not found -->
Causes:
Solutions:
Setup β Embedr((name)) not (name) or {{name}}[Embedr::render] Using visual renderer (template file not found)
Causes:
Solutions:
/site/templates/components/template.phpSetup β Modules β Embedr β Components Pathchmod 644 template.phpCause:
hasField() check)Solution:
embedr-errors log for the exact error$page->hasField('fieldname') guards in your PHP templateAccess β Templates β [Template] β View pages<!-- Embedr: No items found -->
Causes:
Solutions:
Setup β Embedr β Edit β PreviewAccess β Templates β [Template] β View pagesCause:
Solution:
<img src="<?= $page->images->first()->width(400)->url ?>">
Setup β Modules β Embedr β Configure β β Debug Mode
Reproduce the issue Open the page where embed doesnβt work
Setup β Logs β embedr-debug (full log)
Setup β Logs β embedr-errors (errors only)
Embed NOT FOUND β Name mismatchTemplate ERROR β PHP error in templateSelector found 0 items β Permission or selector issueExists: NO β File path problemFix and test
$embedr = $modules->get('Embedr');
$embedr->render('latest-articles');
$embedr->getEmbed('latest-articles');
$embedr->embeds();
$embedr->types();
echo $embedr->renderField($page, 'content_blocks', [
'template' => 'components/content-blocks.php',
'cache' => 3600,
'variables' => ['variant' => 'homepage'],
]);
Field renderer templates receive $page, $field, $value, $rows,
$embedr, and any values supplied through variables. This makes the same API
usable with ProFields Table, Repeater, PageTable, Page Reference, and custom
iterable field values.
Properties:
$embed->id // int - Database ID
$embed->name // string - Unique name (slug)
$embed->title // string - Human-readable title
$embed->type_id // int - Type ID
$embed->selector // string - ProcessWire selector
$embed->type // EmbedrType - Type object
Methods:
$embed->render() // string - Render to HTML
$embed->getType() // EmbedrType|null - Get type object
$embed->getShortcode() // string - Get ((name)) tag
$embed->getCount() // int - Count results without rendering
Methods:
$embedrs->get($name) // EmbedrItem|null - Get by name
$embedrs->getById($id) // EmbedrItem|null - Get by ID
$embedrs->getAll($refresh=false) // WireArray - Get all embeds
$embedrs->save(EmbedrItem $embed) // int|false - Save embed
$embedrs->delete($id) // bool - Delete embed
Properties:
$type->id // int - Database ID
$type->name // string - Unique name
$type->title // string - Display title
$type->icon // string - FA icon name
$type->template // string - PHP template filename
$type->mode // string - 'array' or 'once'
Methods:
$type->getTemplatePath() // string - Full path to template
$type->templateExists() // bool - Check if file exists
Methods:
$types->get($name) // EmbedrType|null - Get by name
$types->getById($id) // EmbedrType|null - Get by ID
$types->getAll($refresh=false) // WireArray - Get all types
$types->save(EmbedrType $type) // int|false - Save type
$types->delete($id) // bool - Delete type
Embeds:
latest-articleshome-featured-products not products1Types:
article not articlesproduct not wine-productGood:
template=article, sort=-created, limit=6
template=product, category=wine, limit=12
parent=/products/, limit=24
Avoid:
template=5314 // Use template name, not ID (both work, but name is clearer)
title%=test // Avoid test data in production
limit=1000 // Too many results
/site/templates/
βββ components/ # Embedr templates
β βββ articles.php
β βββ products.php
β βββ gallery.php
βββ layouts/ # Page layouts
βββ partials/ # Includes
Do:
Donβt:
Always:
$page->hasField('field')htmlspecialchars()Never:
Author: Maxim Semenov
Email: maxim@smnv.org
GitHub: github.com/mxmsmnv/Embedr
Please include:
Setup β Logs β embedr-debug)MIT License β free to use in personal and commercial projects.