PHPackages                             alleyinteractive/wp-block-converter - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. alleyinteractive/wp-block-converter

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

alleyinteractive/wp-block-converter
===================================

Convert HTML into Gutenberg Blocks with PHP

v1.8.2(8mo ago)68460.5k↓33.6%5[2 issues](https://github.com/alleyinteractive/wp-block-converter/issues)1GPL-2.0-or-laterPHPPHP ^8.2CI passing

Since Dec 20Pushed 1w ago19 watchersCompare

[ Source](https://github.com/alleyinteractive/wp-block-converter)[ Packagist](https://packagist.org/packages/alleyinteractive/wp-block-converter)[ Docs](https://github.com/alleyinteractive/wp-block-converter)[ RSS](/packages/alleyinteractive-wp-block-converter/feed)WikiDiscussions develop Synced 2w ago

READMEChangelog (10)Dependencies (8)Versions (28)Used By (1)

WP Block Converter
==================

[](#wp-block-converter)

[![Testing Suite](https://github.com/alleyinteractive/wp-block-converter/actions/workflows/all-pr-tests.yml/badge.svg)](https://github.com/alleyinteractive/wp-block-converter/actions/workflows/all-pr-tests.yml)

Convert HTML into Gutenberg Blocks with PHP

Installation
------------

[](#installation)

Requires PHP 8.4 or later, since HTML parsing is handled by the `Dom\HTMLDocument` API.

You can install the package via Composer:

```
composer require alleyinteractive/wp-block-converter
```

This package does not use any NPM library such as `@wordpress/blocks` to convert HTML to blocks, and — aside from the optional `WordPressImageUploader` described below — has no WordPress dependency of its own, so it can be used inside a WordPress plugin/theme or in a plain PHP project.

Usage
-----

[](#usage)

Use this package like so to convert HTML into Gutenberg Blocks:

```
use Alley\WP\BlockConverter\BlockConverter;

$converter = new BlockConverter( 'Some HTML' );

$blocks = $converter->convert(); // Returns a string of converted blocks.
```

### Logging

[](#logging)

Pass a PSR-3 `LoggerInterface` into the `logger` constructor parameter to receive error-level log entries when an individual image fails to sideload (the conversion otherwise continues without that image):

```
use Alley\WP\BlockConverter\BlockConverter;

$converter = new BlockConverter(
	html: 'Some HTML',
	logger: $psrLogger,
);
```

### Filtering the Blocks

[](#filtering-the-blocks)

The blocks can be filtered on a block-by-block basis or for an entire HTML body by passing closures into the `BlockConverter` constructor.

#### `onBlock`

[](#onblock)

Filter the generated block for a specific node.

```
use Alley\WP\BlockConverter\Block;
use Alley\WP\BlockConverter\BlockConverter;

$converter = new BlockConverter(
	html: 'Some HTML',
	onBlock: function ( ?Block $block, \Dom\Node $node ): ?Block {
		// Modify the block before it is serialized.
		$block->content = '...';
		$block->blockName = '...';
		$block->attributes = [ ... ];

		return $block;
	},
);
```

#### `onDocumentHtml`

[](#ondocumenthtml)

Filter the generated blocks for an entire HTML body.

```
$converter = new BlockConverter(
	html: 'Some HTML',
	onDocumentHtml: function ( string $blocks, \Dom\HTMLCollection $content ): string {
		// ...
		return $blocks;
	},
);
```

#### Other hooks

[](#other-hooks)

The remaining hook points work the same way — pass a closure into the constructor:

Constructor parameterCalled with`onSkipMinifyBlock``( bool $skipMinifyBlock, string $block, \Dom\Node $node ): bool``onPreSideloadImage``( bool $pre, string $src, \Dom\Node $childNode, BlockConverter $converter ): bool``onSideloadedImage``( string $src, \Dom\Node $childNode ): void``onSanitizedImageUrl``( string $sanitizedUrl, string $url ): string`Each hook accepts a single closure; if you need multiple listeners for the same hook, compose them into one closure yourself.

### Sideloading Images

[](#sideloading-images)

By default, `BlockConverter` leaves `` sources untouched — no HTTP requests are made and no images are downloaded. To sideload images, pass an `ImageUploader` implementation into the `uploader` constructor parameter. Inside WordPress, pass `WordPressImageUploader`, which sideloads into the media library:

```
use Alley\WP\BlockConverter\BlockConverter;
use Alley\WP\BlockConverter\WordPressImageUploader;

$converter = new BlockConverter(
	html: 'Some HTML ',
	uploader: new WordPressImageUploader(),
);

$blocks = $converter->convert();
```

Outside of WordPress (or if you want different sideloading behavior inside WordPress), implement the `ImageUploader` interface yourself:

```
use Alley\WP\BlockConverter\ImageUploader;

class MyImageUploader implements ImageUploader {
	public function upload( string $src, string $alt ): string {
		// Download $src and return the URL where it now lives.
		return $src;
	}

	public function attachmentIdFor( string $url ): ?int {
		// Return an ID for the uploaded image if your storage has one, or null.
		return null;
	}

	public function getCreatedAttachmentIds(): array {
		// No-op if your storage has no "attachment" concept.
		return [];
	}

	public function assignParentToAttachments( int $parentPostId ): void {
		// No-op if your storage has no "attachment" concept.
	}
}
```

### Attachment Parents

[](#attachment-parents)

When converting HTML to blocks with a `WordPressImageUploader` (or any `ImageUploader` that tracks attachment IDs), you may need to attach the images that were sideloaded to a post parent. After the HTML is converted to blocks, you can get the attachment IDs that were created or simply attach them to a post.

```
$converter = new BlockConverter(
	html: 'Some HTML ',
	uploader: new WordPressImageUploader(),
);
$blocks = $converter->convert();

// Get the attachment IDs that were created.
$attachmentIds = $converter->getCreatedAttachmentIds();

// Attach the images to a post.
$parentId = 123;
$converter->assignParentToAttachments( $parentId );
```

### Extending the Converter with Macros

[](#extending-the-converter-with-macros)

You can extend the converter with macros to add custom tags that are not yet supported by the converter.

```
use Alley\WP\BlockConverter\BlockConverter;
use Alley\WP\BlockConverter\Block;

BlockConverter::macro( 'special-tag', function ( \Dom\Node $node ) {
	return new Block( 'core/paragraph', [], $node->textContent );
} );

// You can also use the raw HTML with a helper method from Block Converter:
BlockConverter::macro( 'special-tag', function ( \Dom\Node $node ) {
	return new Block( 'core/paragraph', [], BlockConverter::getNodeHtml( $node ) );
} );
```

Macros can also completely override the default behavior of the converter. This is useful when you need to make one-off changes to the way the converter works for a specific tag.

```
use Alley\WP\BlockConverter\BlockConverter;
use Alley\WP\BlockConverter\Block;

BlockConverter::macro( 'p', function ( \Dom\Node $node ) {
	if ( special_condition() ) {
		return new Block( 'core/paragraph', [ 'attribute' => 123 ], 'This is a paragraph' );
	}

	return BlockConverter::p( $node );
} );
```

### Rich Embeds

[](#rich-embeds)

URLs on their own line (e.g. a link to a tweet or a YouTube video) are converted into the corresponding embed block (Twitter/X, Instagram, Facebook, YouTube, Vimeo, and other providers WordPress core supports via oEmbed) using a static, hardcoded provider table rather than a live oEmbed HTTP request — so embed URLs convert identically with or without WordPress loaded. The trade-off: some providers (notably YouTube) vary details like aspect ratio per-URL in ways that normally require an oEmbed response to detect; the provider table uses sensible fixed defaults instead. URLs that don't match a known provider fall back to a plain link/paragraph. If you need live oEmbed responses, you can do this yourself by filtering the block output using a closure passed to the constructor, either using core WordPress functions if you are running your conversion in a WordPress install, or using pure PHP.

Using outside of WordPress
--------------------------

[](#using-outside-of-wordpress)

`BlockConverter` has no WordPress dependency of its own — the only WordPress-specific code in this package is the optional `WordPressImageUploader` class described in [Sideloading Images](#sideloading-images) above. By default (`new BlockConverter( $html )`, no `uploader` passed), converting HTML to blocks runs entirely in plain PHP: no WordPress functions, classes, globals, or database access, and no HTTP calls.

- If you don't need image sideloading, no further setup is required — just require this package with Composer and call `BlockConverter::convert()`.
- If you do need image sideloading outside of WordPress, supply your own `ImageUploader`implementation (see [Sideloading Images](#sideloading-images)) instead of `WordPressImageUploader`, which throws if WordPress isn't loaded.

WP-CLI Command
--------------

[](#wp-cli-command)

This package includes a `ConvertToBlocksCommand` class to bulk convert posts from HTML to Gutenberg blocks, using [wp-bulk-task](https://github.com/alleyinteractive/wp-bulk-task) for efficient processing of large numbers of posts with resume support. The class is not registered with WP-CLI automatically — register it yourself (e.g. in your plugin or theme's `functions.php`):

```
if ( defined( 'WP_CLI' ) && WP_CLI && class_exists( '\Alley\WP\BlockConverter\ConvertToBlocksCommand' ) ) {
	\WP_CLI::add_command( 'block-converter', \Alley\WP\BlockConverter\ConvertToBlocksCommand::class );
}
```

### Basic Usage

[](#basic-usage)

```
# Convert all published posts to blocks
wp block-converter

# Preview changes without saving (dry run)
wp block-converter --dry-run

# Convert a specific post
wp block-converter --post-id=123

# Convert multiple specific posts
wp block-converter --post-id=123,456,789

# Convert custom post type
wp block-converter --post-type=page

# Convert with image sideloading
wp block-converter --sideload-images

# Reset the cursor to start from the beginning
wp block-converter --rewind
```

### Command Options

[](#command-options)

- `--post-type=` - The post type to convert. Default: `post`
- `--post-status=` - The post status to filter by. Default: `publish`
- `--post-id=` - Comma-separated list of post IDs to convert. If provided, only these posts will be processed.
- `--dry-run` - If present, no updates will be made. Shows what would be changed.
- `--rewind` - Resets the cursor so the next time the command is run it will start from the beginning.
- `--sideload-images` - If present, images will be sideloaded and attached to the post.

### Features

[](#features)

- **Resume Support**: If the command is interrupted, it will resume from where it left off on the next run
- **Progress Bar**: Shows real-time progress during bulk processing
- **Dry Run Mode**: Preview changes before actually modifying posts
- **Smart Skipping**: Automatically skips posts that already have blocks or have empty content
- **Error Handling**: Continues processing even if individual posts fail, with detailed error reporting
- **Statistics**: Displays a summary of processed, converted, skipped, and failed posts

Upgrading from v1.x
-------------------

[](#upgrading-from-v1x)

Version 2.0.0 contains several breaking changes related to a shift in philosophy: this package no longer assumes WordPress is loaded. Previously, `BlockConverter` (formerly `Block_Converter`) threw a `RuntimeException` unless WordPress was present, used WordPress hooks and `wp_oembed_get()` internally, and always sideloaded through the media library. Now WordPress is entirely optional, and every WordPress-specific behavior is something you opt into explicitly rather than something the library assumes.

Specifically:

- **PHP 8.4 is now required**, updated from 8.2 in v1.x.
- **The constructor no longer requires WordPress to be loaded.** `new BlockConverter( $html )`previously threw a `RuntimeException` outside of WordPress; it now works standalone.
- **`wp_block_converter_*` filters/actions were replaced with constructor closures.** Each WordPress hook is now an optional `?Closure` constructor parameter on `BlockConverter`, passed directly instead of registered globally with `add_filter()`/`add_action()`. This also means each hook accepts only a single callback, rather than any number of WordPress listeners. Update your code as follows:

    v1.xv2.0.0`add_filter( 'wp_block_converter_skip_minify_block', ... )``onSkipMinifyBlock` constructor parameter`add_filter( 'wp_block_converter_document_html', ... )``onDocumentHtml` constructor parameter`add_filter( 'wp_block_converter_block', ... )``onBlock` constructor parameter`add_filter( 'wp_block_converter_pre_sideload_image', ... )``onPreSideloadImage` constructor parameter`add_action( 'wp_block_converter_sideloaded_image', ... )``onSideloadedImage` constructor parameter`add_filter( 'wp_block_converter_sanitized_image_url', ... )``onSanitizedImageUrl` constructor parameter
- **Image sideloading is now driven by an `ImageUploader` implementation, not a `sideload_images`boolean.** The `sideload_images` constructor parameter is gone. Pass `uploader: new WordPressImageUploader()` to keep sideloading into the media library exactly as before, pass your own `ImageUploader` implementation to sideload somewhere else, or omit `uploader` entirely to leave images untouched (the new default — v1.x defaulted `sideload_images` to `false` as well, but always required WordPress to be loaded even when not sideloading). See [Sideloading Images](#sideloading-images).
- **Rich embeds no longer make a live oEmbed HTTP request.** `wp_oembed_get()` has been replaced with a static, hardcoded provider table. See [Rich Embeds](#rich-embeds) for the trade-offs.
- **Macros now use Illuminate's `Macroable`** (`illuminate/macroable`) instead of Mantle's. The public `BlockConverter::macro()` API is unchanged, so existing macro registrations don't need to be rewritten.
- **`Concerns\Listens_For_Attachments` was removed** along with `src/helpers.php`. Their attachment-tracking logic moved into `WordPressImageUploader`, which implements the new `ImageUploader` interface. If you called either directly rather than going through `BlockConverter`, switch to `WordPressImageUploader`.
- **Every class, method, property, and variable was renamed to StudlyCaps/camelCase** (PSR-12 adoption, see `docs/adr/0001-adopt-psr-12.md`), and the namespace root itself moved from `Alley\WP\Block_Converter` to `Alley\WP\BlockConverter`. Notably:

    v1.xv2.0.0`Alley\WP\Block_Converter\Block_Converter``Alley\WP\BlockConverter\BlockConverter``Alley\WP\Block_Converter\Image_Uploader``Alley\WP\BlockConverter\ImageUploader``Alley\WP\Block_Converter\WordPress_Image_Uploader``Alley\WP\BlockConverter\WordPressImageUploader``Alley\WP\Block_Converter\Convert_To_Blocks_Command``Alley\WP\BlockConverter\ConvertToBlocksCommand``Block::$block_name``Block::$blockName``get_created_attachment_ids()` / `assign_parent_to_attachments()``getCreatedAttachmentIds()` / `assignParentToAttachments()`Update any code that references these symbols directly, or that subclasses/extends them.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Credits
-------

[](#credits)

This project is actively maintained by [Alley Interactive](https://github.com/alleyinteractive). Like what you see? [Come work with us](https://alley.com/careers/).

- [Sean Fisher](https://github.com/srtfisher)
- [All Contributors](../../contributors)

License
-------

[](#license)

The GNU General Public License (GPL) license. Please see [License File](LICENSE) for more information.

###  Health Score

60

—

FairBetter than 98% of packages

Maintenance81

Actively maintained with recent releases

Popularity50

Moderate usage in the ecosystem

Community24

Small or concentrated contributor base

Maturity70

Established project with proven stability

 Bus Factor1

Top contributor holds 53.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~77 days

Recently: every ~58 days

Total

15

Last Release

249d ago

PHP version history (4 changes)v1.0.0PHP ^8.0

v1.1.0PHP ^8.0|^8.1|^8.2

v1.4.0PHP ^8.1|^8.2

v1.6.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/338d27065b1074f2d66d049d742f22996dd137eef6f91bc8f75350ceee1e8ef2?d=identicon)[srtfisher](/maintainers/srtfisher)

---

Top Contributors

[![kevinfodness](https://avatars.githubusercontent.com/u/2650828?v=4)](https://github.com/kevinfodness "kevinfodness (84 commits)")[![srtfisher](https://avatars.githubusercontent.com/u/346399?v=4)](https://github.com/srtfisher "srtfisher (40 commits)")[![mogmarsh](https://avatars.githubusercontent.com/u/11542164?v=4)](https://github.com/mogmarsh "mogmarsh (20 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (6 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (6 commits)")[![Copilot](https://avatars.githubusercontent.com/in/1143301?v=4)](https://github.com/Copilot "Copilot (1 commits)")

---

Tags

wordpresswordpress-packagealleyinteractivewp-block-converter

### Embed Badge

![Health badge](/badges/alleyinteractive-wp-block-converter/health.svg)

```
[![Health](https://phpackages.com/badges/alleyinteractive-wp-block-converter/health.svg)](https://phpackages.com/packages/alleyinteractive-wp-block-converter)
```

###  Alternatives

[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.7k39.6k](/packages/matomo-matomo)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

568591.1k63](/packages/ecotone-ecotone)[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

762297.9k53](/packages/civicrm-civicrm-core)[illuminate/broadcasting

The Illuminate Broadcasting package.

7127.4M237](/packages/illuminate-broadcasting)[logiscape/mcp-sdk-php

Model Context Protocol SDK for PHP

367137.2k16](/packages/logiscape-mcp-sdk-php)[alleyinteractive/wp-curate

Plugin to curate homepages and other landing pages

11268.5k](/packages/alleyinteractive-wp-curate)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
