ueberdosis/tiptap-php
PHP utilities for working with Tiptap: parse and validate ProseMirror/Tiptap JSON, render or transform documents, and build extensions-friendly pipelines on the backend. Ideal for Laravel apps needing server-side handling of rich-text editor content.
Installation:
composer require ueberdosis/tiptap-php
Requires PHP 8.0+.
First Use Case: Convert HTML to Tiptap JSON:
use Tiptap\Editor;
$editor = new Editor();
$json = $editor->setContent('<p>Hello, Tiptap!</p>')->getJSON();
Or JSON to HTML:
$html = (new Editor())
->setContent(['type' => 'doc', 'content' => [...]])
->getHTML();
setContent(), getHTML(), getJSON(), getText(), sanitize().StarterKit (default), CodeBlockHighlight, CodeBlockShiki, Link, etc.// HTML → JSON → Sanitized HTML → Plain Text
$editor = new Editor();
$json = $editor->setContent('<p>Unsafe <script>...</script></p>')->getJSON();
$sanitizedHtml = $editor->sanitize($json)->getHTML();
$plainText = $editor->getText(['blockSeparator' => "\n"]);
$editor = new Editor([
'extensions' => [
new StarterKit([
'heading' => ['levels' => [1, 2, 3]],
'codeBlock' => false,
]),
new CodeBlockShiki(['theme' => 'github-dark']),
new Link(['openInNewTab' => true]),
],
]);
$editor = new Editor();
$editor->setContent('<h1>Title</h1><p>Content</p>');
// Modify headings to level 2
$editor->descendants(function (&$node) {
if ($node->type === 'heading') {
$node->attrs->level = 2;
}
});
$updatedHtml = $editor->getHTML();
// Parse uploaded HTML (e.g., from CKEditor/TinyMCE)
$editor = new Editor();
$safeJson = $editor->sanitize($userUploadedHtml)->getJSON();
// Store JSON in DB, render later
$renderedHtml = (new Editor())->setContent($safeJson)->getHTML();
$editor = new Editor();
$metaDescription = $editor
->setContent($articleHtml)
->getText(['blockSeparator' => ' ', 'ellipsis' => '...', 'length' => 160]);
Service Provider Binding:
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(Editor::class, function () {
return new Editor([
'extensions' => [
new StarterKit(),
new Link(),
],
]);
});
}
Form Request Validation:
// app/Http/Requests/StoreArticleRequest.php
public function rules()
{
return [
'content' => 'required|string|tiptap_json', // Custom rule for JSON validation
];
}
Blade Directives for Rendering:
// app/Providers/BladeServiceProvider.php
Blade::directive('tiptap', function ($expression) {
return "<?php echo (new \\Tiptap\\Editor())->setContent({$expression})->getHTML(); ?>";
});
Usage:
{!! tiptap($article->content) !!}
API Response Formatting:
return response()->json([
'html' => $editor->setContent($request->json)->getHTML(),
'text' => $editor->getText(),
]);
CodeBlockShiki, cache the Shiki instance to avoid reinitializing Node.js for each request.Reference Handling in descendants():
& in the callback parameter modifies a copy, not the original node.function (&$node) to modify nodes in-place.
$editor->descendants(function (&$node) { ... }); // Correct
$editor->descendants(function ($node) { ... }); // Incorrect (no effect)
HTML Attribute Conflicts:
data-*) may conflict with Tiptap’s internal parsing.HTMLAttributes in extension config to explicitly define allowed attributes.
new Heading(['HTMLAttributes' => ['class' => 'my-heading']])
Nested Marks:
Mark extensions with parseHTML and renderHTML carefully. Test with complex nested cases.
new Marks\Strong(['parseHTML' => ['strong', 'b']]);
Shiki Dependency:
CodeBlockShiki requires Node.js and shiki npm package, which may not be available in shared hosting.CodeBlockHighlight or disable code blocks if Shiki is unavailable.
try {
new CodeBlockShiki();
} catch (\RuntimeException $e) {
new CodeBlockHighlight();
}
Schema Validation:
if (!json_validate($request->content)) {
throw new \InvalidArgumentException('Invalid Tiptap JSON');
}
Priority Collisions:
class CustomNode extends Node {
public static $priority = 50; // Lower than default
}
Inspect Intermediate JSON:
$json = $editor->setContent($html)->getJSON();
\Log::debug('Tiptap JSON:', ['json' => json_encode($json, JSON_PRETTY_PRINT)]);
Compare HTML Output: Use browser dev tools to compare rendered HTML with expected output. Look for:
<strong> inside <code>).Extension Isolation: Test extensions one by one to identify conflicts:
// Test StarterKit alone
$editor = new Editor(['extensions' => [new StarterKit()]]);
Shiki Debugging: Enable Shiki logging to diagnose issues:
new CodeBlockShiki(['debug' => true]);
StarterKit Shortcuts:
new StarterKit(['heading' => false, 'bulletList' => true])
heading.levels) are supported but may not cover all JS API features.Default Extensions:
StarterKit. To disable it entirely:
new Editor(['extensions' => []])
Sanitization Scope:
sanitize() only removes malicious content (e.g., <script>). For stricter rules, combine with PHP’s filter_var() or HTML purifiers like HTMLPurifier.Plain Text Options:
getText() supports these options:
blockSeparator: String between blocks (default: \n\n).ellipsis: Truncate long text (e.g., '...').length: Max length for truncated text.Custom Nodes/Marks:
Tiptap\Core\Node or Tiptap\Core\Mark for new content types.parseHTML(), renderHTML(), and addOptions() methods.Override Default Rendering:
How can I help you explore Laravel packages today?