nowo-tech/tiptap-editor-bundle
A Symfony bundle that integrates the Tiptap rich text editor into your forms and templates, giving you a clean, extensible WYSIWYG experience with sensible defaults, easy configuration, and no front-end boilerplate.
This is a Symfony bundle, not a Laravel package — that shapes everything below. It ships a Symfony Form type that renders a Tiptap (ProseMirror) editor on top of a plain <textarea>, so the submitted value is HTML that lands in whatever column your entity maps. If you are working in Laravel, the honest first step is to decide whether you are adopting Symfony's Form component (you are not, in a Laravel app) or only borrowing the frontend: the compiled editor lives in src/Resources/public/ as a Vite IIFE bundle, and that single file is the only piece that is portable to a Laravel/Blade context.
For a Symfony app, the minimal path:
composer require nowo-tech/tiptap-editor-bundle.config/bundles.php if Flex did not do it (the class lives at the package root, following the NowoTechnologies\TiptapEditorBundle\... namespace convention).src/DependencyInjection/Configuration.php before writing any YAML — it is the authoritative list of options, and it is short. Do the same for src/Form/ to get the real form type class name and its configureOptions() signature; do not guess it from the README.tiptap-editor.js; check src/Resources/views/ for the Twig template that includes it and confirm whether it is pulled in automatically by the form theme or whether you must add it to your base layout.First use case: take an existing TextareaType field on a CMS-ish entity (a Post::$body or Page::$content mapped as Types::TEXT) and swap the form type. Keep the column nullable, keep your existing validators, and verify with curl/browser devtools that the POST body contains the same HTML the editor showed. That single round trip validates the whole integration: asset loading, form rendering, textarea sync, and persistence.
Entity and storage. Treat the field as an opaque HTML string:
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $body = null;
No JSON column, no ProseMirror doc serialization on your side — the bundle's contract is "HTML in the textarea." That is convenient for migrations and for reading the value in contexts that never load JS (exports, emails, search indexing), but it pushes all sanitization responsibility onto you.
Sanitization belongs on output, not only on input. Store what the editor produced, then sanitize at render time with an allow-list HTML sanitizer, and additionally sanitize on save if untrusted roles can author content. In Twig, {{ post.body|raw }} is the only way to render it, so keep |raw confined to a single, obviously-named sanitized filter/macro rather than sprinkling it through templates.
Form wiring. Instantiate the bundle's form type wherever you previously had TextareaType. Pass toolbar/editor options through the type's own options array (read configureOptions() first — it is the public extension point and the thing most likely to be BC-stable). For an app with several content shapes ("article", "comment", "short bio"), either define one form type per shape that pre-sets the options, or drive them from a single bundle config profile; the former is easier to test and avoids global config drift.
Testing. A TypeTestCase asserting the rendered widget and the presence of the bundle's asset include is cheap and catches 90% of breakage on upgrade. Add one functional test that POSTs HTML containing a link and an emphasis mark and asserts it round-trips.
Turbo / Live Components / partial re-renders. The editor binds to a textarea at page load. Any framework that swaps that DOM node without a full reload will leave an orphaned ProseMirror instance. Plan for a small stimulus/JS hook that re-initializes after the swap, and give each textarea a unique id so multiple editors on one page do not collide.
Version discipline. Zero stars and zero dependents means you are the QA. Pin an exact version in composer.json, and read CHANGELOG.md plus UPGRADING.md on every bump — the bundle's own contributor rules promise BC preservation for DI extension, compiler pass wiring, and form type options, which tells you those are the surfaces to diff.
<p></p> is not empty. An emptied Tiptap editor emits an empty paragraph, so NotBlank on the raw string will pass on visually blank content. Add a normalizing callback (strip tags, trim, treat as blank) before validation.tiptap-editor.js request first, then the console. With AssetMapper you may need to map src/Resources/public/ explicitly; with Webpack Encore, copy or reference the bundle path from your layout. Do not edit anything under src/Resources/public/build.VARCHAR(255) will truncate; use TEXT/LONGTEXT and, if you validate length, validate the stripped text, not the markup.Length/Regex constraints run against raw HTML and produce confusing failures. Normalize (sanitize + strip tags) in a PRE_SUBMIT/POST_SUBMIT listener or a custom constraint before measuring.style-src violation appears — rich text builds commonly inject inline styles.id; duplicate ids break label association and can make the JS attach to the wrong node....Type class that delegates.How can I help you explore Laravel packages today?