anomaly/markdown-field_type
Anomaly Markdown Field Type provides a Markdown-powered field for Laravel Streams, enabling editors to write Markdown content in entries and render it cleanly. It extends Streams field types with a familiar, lightweight authoring experience.
Pull the field type into an existing Streams Platform / PyroCMS install and it self-registers — there is no service provider to wire up manually, the addon boots from composer.json metadata.
composer require anomaly/markdown-field_type
php artisan addon:install anomaly.markdown_field_type
# if the editor assets don't appear in the CP, force a publish:
php artisan addon:publish anomaly.markdown_field_type
Where to look first, in order:
src/MarkdownFieldType.php — the field type class. It almost certainly extends Anomaly\TextFieldType\TextFieldType, which tells you the storage column, validation rules, and hydration behaviour are inherited from the text field type. Only the getView()/getWrapper() methods and asset declarations differ.resources/views/ — the admin input.twig (the <textarea> the JS editor attaches to) and the wrapper.twig (label, instructions, error output).resources/js/ and resources/scss/ — the editor binding (likely EasyMDE/SimpleMDE). This is where the save behaviour lives, so read it before you debug "content disappears on save."config/config.php — editor options (toolbar, autofocus, spellchecker, preview rendering) surfaced to the view.First use case: add a body markdown field to a posts stream via the control panel's field editor (choose Markdown as the type), then render it in a page or module view. Out of the box the field type stores raw markdown; you are responsible for converting it to HTML:
{{ entry.body|raw }} {# WRONG unless something already parsed it #}
so the realistic day-one workflow is: install, assign the field, then add a parsed accessor or presenter (see below) before you display anything.
Storage vs. rendering is deliberately split. Treat the field type as an input widget only. The database column holds the markdown source; conversion is a presentation concern.
The most common pattern is a generated presenter on the entry model:
use League\CommonMark\CommonMarkConverter;
class PostPresenter extends EntryPresenter
{
public function body()
{
return (new CommonMarkConverter([
'html_input' => 'escape',
'allow_unsafe_links' => false,
]))->convert($this->object->body)->getContent();
}
}
Then {{ entry.body|raw }} is safe-ish because the converter is doing the escaping. Cache the converter instance (or resolve it from the container) rather than constructing one per call if you render lists.
Assigning via migration/seeder is the pattern that survives redeploys:
$stream->createField('body', 'markdown')->setConfig([...]);
// or in a seeder: $fields->add(['slug' => 'body', 'type' => 'anomaly.field_type.markdown']);
Two-field pattern for search. Because the column is markdown source, full-text and LIKE queries will match syntax characters and produce noisy results. A frequently used workflow is to keep the markdown field for authoring and mirror a stripped plain-text version into a textarea field (or an indexed column) on save — a form/observer hook on the entry model. It costs one extra column and makes search and excerpt generation trivial.
Integration with the WYSIWYG ecosystem. Since this is a textarea superset, it composes nicely with anything that reads the entry as a string: RSS/Atom feed builders, Str::words() excerpts, meta-description generation, and email templates. It does not compose with the WYSIWYG field's HTML-specific tooling (image pickers, file manager bindings, shortcode expansion) — markdown has no equivalent, so plan for authors pasting image URLs by hand.
Editor-level customisation is done through the field's config (persisted per-field in the control panel, or via setConfig()), not by editing the addon — override the addon view in your own namespace if you need a toolbar change, so composer update doesn't eat it.
{{ entry.body }} emits source text. If you see ## Heading on the page, you haven't added a converter, not a field-type bug.html_input => 'escape' or run HTMLPurifier after conversion.VARCHAR(255)-ish column. Markdown posts blow past that and you get silent truncation. Use TEXT/LONGTEXT — in the CP field editor, check "Max Length" is empty.<textarea> with no toolbar. Fix in this order: php artisan addon:publish, then php artisan asset:clear / view:clear, then check browser console for a 404 on the published path. If you're behind a path prefix or CDN, the published asset URLs in the field view may need overriding.change; if a form is submitted programmatically or the field is inside a hidden tab/repeater, the sync may not fire and the server receives an empty string. Force a sync before submit (editor.codemirror.save() in EasyMDE terms) or read the wrapper in the addon view to see how it binds.$(document).ready won't bind to rows cloned by the repeater addon. You typically need to re-init on the repeater's row-added event — this is a common reason to copy the addon's JS into your own theme.anomaly/textarea-field_type (the parent) and anomaly/wysiwyg-field_type are the authoritative patterns for views, config, and asset registration.getView(), rather than editing the vendor addon.How can I help you explore Laravel packages today?