A Laravel wrapper for the Tiptap PHP package that provides a fluent, Laravel-friendly interface for processing rich text content. Transforms JSON to HTML, HTML to JSON, validates content structure, extracts statistics etc. with an elegant API that feels like home in a Laravel app.
You can install the package via composer:
composer require rjp2525/laravel-tiptap
You can publish the config file with:
php artisan vendor:publish --tag="tiptap-config"
This is the contents of the published config file:
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Extensions
|--------------------------------------------------------------------------
|
| Default extensions to be loaded when creating a new Tiptap instance.
| Available extensions: StarterKit, Color, FontFamily, TextAlign
|
*/
'extensions' => [
// Use StarterKit for basic functionality
\Tiptap\Extensions\StarterKit::class => [],
// Additional extensions (uncomment as needed)
// \Tiptap\Extensions\Color::class => [],
// \Tiptap\Extensions\FontFamily::class => [],
// \Tiptap\Extensions\TextAlign::class => [],
],
/*
|--------------------------------------------------------------------------
| Cache Configuration
|--------------------------------------------------------------------------
|
| Configure caching for parsed content to improve performance.
|
*/
'cache' => [
'enabled' => env('TIPTAP_CACHE_ENABLED', true),
'store' => env('TIPTAP_CACHE_STORE', null), // null uses default cache store
'ttl' => env('TIPTAP_CACHE_TTL', 3600), // 1 hour
'prefix' => 'tiptap',
],
/*
|--------------------------------------------------------------------------
| Validation Rules
|--------------------------------------------------------------------------
|
| Default validation rules for content validation
|
*/
'validation' => [
'max_length' => 50000,
'max_depth' => 10,
'allowed_tags' => null, // null means all configured extensions are allowed
],
];
Optionally, you can publish the views using
php artisan vendor:publish --tag="laravel-tiptap-views"
use RJP\Tiptap\Facades\Tiptap;
// Parse JSON to HTML
$json = [
'type' => 'doc',
'content' => [
[
'type' => 'paragraph',
'content' => [
['type' => 'text', 'text' => 'Hello '],
['type' => 'text', 'marks' => [['type' => 'bold']], 'text' => 'world!'],
]
]
]
];
$html = Tiptap::parseJson($json);
// Output: <p>Hello <strong>world!</strong></p>
// Parse HTML back to JSON
$json = Tiptap::parseHtml('<p>Hello <strong>world!</strong></p>');
// Validate content
$isValid = Tiptap::validate($json, ['max_length' => 1000]);
// Get content statistics
$stats = Tiptap::getStats($json);
// Returns: ['characters' => 12, 'words' => 2, 'paragraphs' => 1, 'reading_time' => 1]
use RJP\Tiptap\Facades\Tiptap;
// Content processing with method chaining
$result = Tiptap::make()
->content($jsonContent)
->starterKit()
->color()
->textAlign(['types' => ['heading', 'paragraph']])
->maxLength(5000)
->maxDepth(10)
->validateOrFail()
->sanitize()
->toHtml();
// Content analysis
$stats = Tiptap::make()
->content($content)
->stats(); // Returns Collection
$wordCount = Tiptap::make()->content($content)->wordCount();
$readingTime = Tiptap::make()->content($content)->readingTime(); // in minutes
$isEmpty = Tiptap::make()->content($content)->isEmpty();
Configure extensions with specific options:
// In config/tiptap.php or dynamically
$result = Tiptap::make()
->content($content)
->starterKit([
'heading' => ['levels' => [1, 2, 3]], // Only H1-H3
'bold' => false, // Disable bold
'bulletList' => [],
'orderedList' => [],
])
->color()
->textAlign([
'types' => ['heading', 'paragraph'],
'alignments' => ['left', 'center', 'right'],
])
->toHtml();
// Custom validation rules
$isValid = Tiptap::make()
->content($content)
->rules([
'max_length' => 10000,
'max_depth' => 8,
'allowed_tags' => ['paragraph', 'heading', 'text', 'bold', 'italic']
])
->validate();
// Fluent validation methods
$builder = Tiptap::make()
->content($content)
->maxLength(5000)
->maxDepth(5)
->allowedTags(['paragraph', 'text', 'bold'])
->validateOrFail(); // Throws InvalidArgumentException if validation fails
$builder = Tiptap::make()
->content($content)
->starterKit()
->when($user->isPremium(), fn($b) => $b->color())
->when($user->canUseAdvancedFormatting(), fn($b) => $b->fontFamily())
->unless($content->isEmpty(), fn($b) => $b->sanitize())
->tap(function ($builder) {
logger('Processing content', [
'word_count' => $builder->wordCount(),
'reading_time' => $builder->readingTime(),
]);
});
Enable caching for better performance with frequently processed content
// In your .env file
TIPTAP_CACHE_ENABLED=true
TIPTAP_CACHE_TTL=3600
TIPTAP_CACHE_STORE=redis
// Or disable caching for specific operations
$result = Tiptap::make()
->content($content)
->withoutCache()
->toHtml();
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use RJP\Tiptap\Facades\Tiptap;
class CreateArticleRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'array', function ($attribute, $value, $fail) {
if (!Tiptap::validate($value, ['max_length' => 50000, 'max_depth' => 10])) {
$fail('The content is invalid, too long, or too deeply nested.');
}
}],
];
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use RJP\Tiptap\Facades\Tiptap;
class Article extends Model
{
protected $fillable = ['title', 'content'];
protected $casts = ['content' => 'array'];
// Automatically generate HTML from JSON content
protected function contentHtml(): Attribute
{
return Attribute::make(
get: fn () => Tiptap::parseJson($this->content),
);
}
// Get content statistics
protected function contentStats(): Attribute
{
return Attribute::make(
get: fn () => collect(Tiptap::getStats($this->content)),
);
}
// Get plain text version
protected function contentText(): Attribute
{
return Attribute::make(
get: fn () => Tiptap::toText($this->content),
);
}
// Sanitize content before saving
protected static function boot()
{
parent::boot();
static::saving(function ($article) {
if ($article->isDirty('content')) {
$article->content = Tiptap::make()
->content($article->content)
->validateOrFail()
->sanitize()
->raw();
}
});
}
}
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateArticleRequest;
use App\Models\Article;
use RJP\Tiptap\Facades\Tiptap;
class ArticleController extends Controller
{
public function store(CreateArticleRequest $request)
{
$processedContent = Tiptap::make()
->content($request->validated('content'))
->starterKit()
->color()
->maxLength(50000)
->validateOrFail()
->sanitize()
->raw();
$stats = Tiptap::make()
->content($processedContent)
->stats();
$article = Article::create([
'title' => $request->validated('title'),
'content' => $processedContent,
'word_count' => $stats['words'],
'reading_time' => $stats['reading_time'],
]);
return response()->json([
'article' => $article,
'stats' => $stats,
'html_preview' => $article->content_html,
]);
}
}
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RJP\Tiptap\Facades\Tiptap;
use App\Models\Article;
class ProcessArticleContentJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private int $articleId,
private array $content
) {}
public function handle(): void
{
$processed = Tiptap::make()
->content($this->content)
->starterKit()
->color()
->textAlign()
->maxLength(100000)
->sanitize()
->raw();
$stats = Tiptap::make()
->content($processed)
->stats();
Article::where('id', $this->articleId)->update([
'content' => $processed,
'content_html' => Tiptap::parseJson($processed),
'content_text' => Tiptap::toText($processed),
'word_count' => $stats['words'],
'character_count' => $stats['characters'],
'reading_time' => $stats['reading_time'],
'paragraph_count' => $stats['paragraphs'],
'processed_at' => now(),
]);
}
}
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use RJP\Tiptap\Facades\Tiptap;
class TiptapContent extends Component
{
public function __construct(
public array|string $content,
public bool $showStats = false,
public bool $sanitize = true,
) {}
public function render()
{
$builder = Tiptap::make()->content($this->content);
if ($this->sanitize) {
$builder->sanitize();
}
$html = is_array($this->content)
? $builder->toHtml()
: $this->content;
$stats = $this->showStats
? $builder->stats()
: null;
return view('components.tiptap-content', [
'html' => $html,
'stats' => $stats,
]);
}
}
While the Tiptap PHP package currently supports StarterKit, Color, FontFamily, and TextAlign extensions, you can register custom extensions by extending the service:
// In a service provider
use RJP\Tiptap\Tiptap;
$this->app->extend(TiptapInterface::class, function (Tiptap $service) {
// Add your custom extension logic here
// Note: This requires extending the underlying Tiptap PHP package
return $service;
});
For custom nodes and marks, you'll need to create them following the Tiptap PHP documentation and then register them through the configuration.
You can create your own service implementation for advanced customization:
<?php
namespace App\Services;
use RJP\Tiptap\Contracts\TiptapInterface;
use RJP\Tiptap\Tiptap;
class CustomTiptap implements TiptapInterface
{
public function __construct(private Tiptap $tiptap) {}
// Delegate to original service
public function parseJson(array|string $json, array $extensions = []): string
{
return $this->tiptap->parseJson($json, $extensions);
}
// Add custom methods
public function parseMarkdown(string $markdown): array
{
// Your markdown parsing logic
$json = $this->convertMarkdownToTiptapJson($markdown);
return $json;
}
public function extractImages(array|string $content): array
{
// Extract image nodes from content
return $this->findImageNodes($content);
}
// Implement other interface methods...
}
// Register in AppServiceProvider
$this->app->bind(TiptapInterface::class, CustomTiptap::class);
The TiptapBuilder
class is designed to be extended as well:
<?php
namespace App\Services;
use RJP\Tiptap\Builders\TiptapBuilder;
class CustomTiptapBuilder extends TiptapBuilder
{
/**
* Add markdown support to the fluent interface.
*/
public function markdown(string $markdown): self
{
$json = $this->convertMarkdownToJson($markdown);
return $this->content($json);
}
/**
* Enforce word limit validation.
*/
public function wordLimit(int $limit): self
{
return $this->tap(function ($builder) use ($limit) {
if ($builder->wordCount() > $limit) {
throw new InvalidArgumentException("Content exceeds {$limit} word limit");
}
});
}
/**
* Add reading level analysis.
*/
public function readingLevel(): array
{
$text = $this->toText();
return $this->calculateReadingLevel($text);
}
private function convertMarkdownToJson(string $markdown): array
{
// Your markdown to Tiptap JSON conversion logic
return [];
}
private function calculateReadingLevel(string $text): array
{
// Your reading level calculation logic
return ['grade' => 8, 'difficulty' => 'medium'];
}
}
Then create a custom service that uses your builder:
<?php
namespace App\Services;
use RJP\Tiptap\Tiptap;
class ExtendedTiptap extends Tiptap
{
public function make(): CustomTiptapBuilder
{
return new CustomTiptapBuilder($this);
}
}
Add methods to the existing builder without extending:
// In a service provider boot method
use RJP\Tiptap\Builders\TiptapBuilder;
TiptapBuilder::macro('seoAnalysis', function () {
return [
'word_count' => $this->wordCount(),
'reading_time' => $this->readingTime(),
'headings' => $this->extractHeadings(),
'images' => $this->extractImages(),
];
});
// Usage
$seoData = Tiptap::make()
->content($content)
->seoAnalysis();
The package currently supports these extensions from Tiptap PHP:
- StarterKit: Includes essential nodes (document, paragraph, text, heading, etc.) and marks (bold, italic, strike, etc.)
- Color: Text and background color support
- FontFamily: Font family formatting
- TextAlign: Text alignment (left, center, right, justify)
// Direct methods
Tiptap::parseJson(array|string $json, array $extensions = []): string
Tiptap::parseHtml(string $html, array $extensions = []): array
Tiptap::validate(array|string $content, array $rules = []): bool
Tiptap::sanitize(array|string $content, array $extensions = []): array|string
Tiptap::toText(array|string $content): string
Tiptap::getStats(array|string $content): array
// Builder factory
Tiptap::make(): TiptapBuilder
// Content and extensions
->content(array|string $content)
->extensions(array $extensions)
->starterKit(array $options = [])
->color()
->fontFamily()
->textAlign(array $options = [])
// Validation
->rules(array $rules)
->maxLength(int $length)
->maxDepth(int $depth)
->allowedTags(array $tags)
->validate(): bool
->validateOrFail(): self
// Processing
->sanitize(): self
->withCache() / ->withoutCache()
// Output
->toHtml(): string
->toJson(): array
->toText(): string
->stats(): Collection
->wordCount(): int
->characterCount(bool $includeSpaces = true): int
->readingTime(): int
->isEmpty() / ->isNotEmpty(): bool
->raw(): array|string|null
// Utilities
->when(bool $condition, callable $callback): self
->unless(bool $condition, callable $callback): self
->tap(callable $callback): self
->clone(): self
composer test
Please see CHANGELOG for more information on what has changed recently.
- rjp2525
- Built on top of the official Tiptap PHP by ueberdosis
- All Contributors
The MIT License (MIT). Please see License File for more information.