ueberdosis/tiptap-php
Convert Tiptap content between HTML and JSON in PHP. Set content, get documents, JSON, or HTML, sanitize and modify content, and extend it with custom nodes and marks. A PHP port of the Tiptap editor API.
What the package is: a server-side PHP implementation of the Tiptap/ProseMirror document model — it parses HTML into Tiptap-compatible JSON, serializes JSON back to HTML, flattens to plain text, sanitizes, and allows tree traversal/mutation via descendants(). It is a content-transformation library, not a framework component.
Architecture fit: Strong where the application has a Tiptap editor on the front end and needs PHP-side handling of the same document format. The natural seams are: (1) render-on-write — convert stored JSON to HTML at publish/save time so public pages and emails don't depend on a PHP renderer per request; (2) sanitize-on-write — enforce an allowlist at the persistence boundary as defense-in-depth alongside client-side sanitization; (3) derive read models — getText() for search indices, excerpts, notification previews, and RSS; (4) content ops — batch migrations and attribute normalization over existing documents. It is a poor fit as a general-purpose HTML sanitizer for untrusted third-party HTML, and a poor fit if the HTML→JSON direction is expected to be byte-faithful.
Integration feasibility: Low friction. Composer install, no Laravel service provider required, no database or queue coupling, MIT license, no runtime services for the default path. The API deliberately mirrors the JavaScript package (new Editor([...])->setContent()->getHTML()), so developers already fluent in Tiptap need minimal translation. Extension configuration (StarterKit, per-node options, HTMLAttributes) maps onto the JS mental model, which reduces design ambiguity in code review.
Technical risk — the dominant one is schema parity. The README itself states the implementation "doesn't fully adhere to the ProseMirror schema" (e.g. marks in CodeBlock). Every custom node or mark in the JS editor must be reimplemented in PHP (Node/Mark subclasses with parseHTML, renderHTML, priorities). Any drift produces silent divergence: content authored in the editor that the PHP side does not recognize can be dropped or flattened during a sanitize-then-persist cycle. This is a data-loss risk, not a rendering bug, and it must be treated as such in the design. Priority ordering ($priority, default 100) also becomes an application-level concern once custom nodes overlap with built-ins (TaskItem vs ListItem is the documented precedent).
Secondary risks:
sanitize() normalizes against the loaded extension set — it removes what the schema does not know about, which is exactly the failure mode above if the extension set is incomplete. Treat it as schema conformance, not as a security boundary by itself.TypeError on non-string link hrefs, 2.1.2 fixed non-scalar attribute crashes. These are correctness/robustness fixes on the serialization path — good signs of maintenance, but evidence that attribute handling has been a live edge. Pin versions and re-run a parity corpus on upgrade.CodeBlockShiki shells out to Shiki via spatie/shiki-php and requires the shiki npm package. That converts a pure-PHP library into something with a Node runtime on the server. Avoid unless there is a hard requirement; CodeBlockHighlight (highlight.php) is the in-process alternative.Key questions for the team:
Stack fit: Clean. Plain PHP + Composer, MIT, no Laravel coupling and no global state — easy to wrap in a small application service (e.g. ContentRenderer) and bind via the container so the editor configuration lives in one place (config/tiptap.php) rather than being reconstructed at call sites. Keep it behind that seam so the package can be swapped or upgraded without touching controllers, Blade templates, or jobs.
Suggested placement:
App\Support\Content\TiptapRenderer — single extension registry, one place where StarterKit and any custom nodes are declared.RenderContentHtml action invoked from model observers/jobs at publish time, persisting content_html and content_text.SanitizeContent action at the persistence boundary for editor saves.Migration path: Incremental and reversible. Start as a read-only derivative generator against an existing corpus — no writes, no schema changes, no user-visible behavior. Compare output against the current renderer for a sample of real documents and quantify divergence before committing to anything. Only after parity is acceptable do you move sanitize-on-write and any mutation workflows into the critical path.
Compatibility considerations:
^2.2) and record the resolved version; attribute-handling behavior has changed across 2.1.x→2.2.0.Details (added 2.1.0), Image width/height attributes (2.1.0), and link mark edge cases. Anything not covered is a candidate for silent drop.Sequencing:
Maintenance: Low ongoing cost, but not zero. Single-vendor stewardship (ueberdosis, the company behind Tiptap) is strategically favorable — this package exists to support their editor product, so it is unlikely to be abandoned — but it is a companion library, not the flagship, so PHP-side features will lag the JS feature set and requests for parity may be deprioritized. Plan for the possibility of contributing upstream patches for custom-node gaps. MIT license removes legal friction; the bus factor is the vendor's priorities, not a legal or licensing risk.
Support: Community-driven via GitHub issues and Discord; no commercial SLA implied by the MIT package. Internal ownership matters more than vendor support: name one engineer as the parity owner so that editor-configuration changes trigger a PHP-side review. Budget a small recurring allowance for upstream fixes, particularly around attribute serialization where recent history shows activity.
Scaling: CPU-bound DOM parsing and serialization in PHP, roughly linear in document size, with per-document cost driven by extension count and node depth. This scales acceptably for write-time rendering and poorly for read-time rendering of high-traffic pages. Design accordingly: render once at publish, cache the HTML in a column or object store, and never invoke the editor on a hot read path. For backfills over large corpora, run through queued jobs with bounded concurrency, chunked by document, and expect to throttle — this is a batch CPU workload, not an I/O workload. The Shiki path multiplies cost per code block by spawning a Node process; if used, restrict it to publish-time and batch it, or exclude it entirely.
Failure modes to instrument:
javascript: hrefs, event-handler attributes, nested script).CodeBlockHighlight, or gate Shiki behind a health check and fall back to plain code blocks.Ramp-up: Estimated 2–4 days for a senior Laravel engineer to be productive and 1–2 weeks to be genuinely safe, with most of that time spent on the parity harness rather than the library itself. The API mirrors the JS package, so the conceptual load is low for anyone who has touched Tiptap; the real learning is the extension model (parseHTML/renderHTML/getAttrs/priority semantics) and the discipline of keeping JS and PHP extension sets in lockstep. Two artifacts accelerate onboarding more than any documentation: the fixture corpus with expected outputs, and a one-page "how to add a custom node to both sides" runbook.
Bottom line for a TPM: inexpensive, low-coupling, high-leverage for the specific job of rendering and indexing Tiptap content server-side. The opportunity is real (content-heavy Laravel apps routinely need it), but the score/opportunity asymmetry in the assessment reflects that the library is small-surface and single-maintainer — the risk is not adoption cost, it is parity drift and silent data loss. Fund the parity harness and the write-time rendering design before funding the integration.
How can I help you explore Laravel packages today?