The Notion2WP plugin uses a modular block converter system to transform Notion blocks into WordPress Gutenberg blocks. This architecture provides flexibility, maintainability, and extensibility.
-
Block_Converter_Interface (
interface-block-converter.php)- Defines the contract for all block converters
- Methods:
supports(),convert(),get_priority()
-
Abstract_Block_Converter (
abstract-block-converter.php)- Base class with common functionality
- Provides utility methods for rich text conversion, color handling, and child block processing
- All converters extend this class
-
Block_Registry (
class-block-registry.php)- Singleton that manages all block converters
- Handles converter registration and priority sorting
- Routes blocks to the appropriate converter
-
Individual Converters (
converters/)- Each Notion block type has its own converter class
- Implements the conversion logic for that specific block type
| Notion Block Type | Converter Class | Gutenberg Block |
|---|---|---|
| paragraph | Paragraph_Converter |
core/paragraph |
| heading_1/2/3 | Heading_Converter |
core/heading |
| bulleted_list_item | List_Converter |
core/list |
| numbered_list_item | List_Converter |
core/list |
| quote | Quote_Converter |
core/quote |
| code | Code_Converter |
core/code |
| image | Image_Converter |
core/image |
| divider | Divider_Converter |
core/separator |
| callout | Callout_Converter |
core/group |
| toggle | Toggle_Converter |
core/details |
| to_do | Todo_Converter |
core/list |
| column_list | Column_Converter |
core/columns |
| table | Table_Converter |
core/table |
| bookmark | Bookmark_Converter |
core/embed |
| embed | Embed_Converter |
core/embed |
| file | File_Converter |
core/file |
| video | Video_Converter |
core/video or core/embed |
| audio | Audio_Converter |
core/audio |
| (unsupported) | Unsupported_Converter |
HTML comment |
When the Block_Registry is instantiated, it automatically registers all converters:
$registry = Block_Registry::get_instance();
// All converters are now registeredNotion Block
↓
Block_Registry::convert_block()
↓
Find matching converter (highest priority first)
↓
Converter::supports() - Check if converter handles this block
↓
Converter::convert() - Transform to Gutenberg format
↓
Process children recursively (if any)
↓
Gutenberg Block HTML
Nested blocks (children) are handled automatically:
protected function process_children( $children, $context = [] ) {
$registry = Block_Registry::get_instance();
$html = '';
foreach ( $children as $child_block ) {
$html .= $registry->convert_block( $child_block, $context );
}
return $html;
}Create a new file in includes/blocks/converters/:
<?php
namespace Notion2WP\Blocks\Converters;
use Notion2WP\Blocks\Abstract_Block_Converter;
defined( 'ABSPATH' ) || exit;
class My_Block_Converter extends Abstract_Block_Converter {
public function supports( $block ) {
return isset( $block['type'] ) && 'my_block_type' === $block['type'];
}
public function convert( $block, $context = [] ) {
$block_data = $block['my_block_type'] ?? [];
$rich_text = $block_data['rich_text'] ?? [];
// Convert rich text to HTML
$content = $this->rich_text_to_html( $rich_text );
// Build HTML
$html = '<div>' . $content . '</div>';
// Process children if present
if ( ! empty( $block['children'] ) ) {
$html .= $this->process_children( $block['children'], $context );
}
// Wrap in Gutenberg block format
return $this->wrap_gutenberg_block( 'core/my-block', $html );
}
}Add to Block_Registry::register_default_converters():
require_once __DIR__ . '/converters/class-my-block-converter.php';
$this->register( new Converters\My_Block_Converter() );// Get plain text only
$text = $this->extract_plain_text( $rich_text );
// Get HTML with formatting (bold, italic, links, etc.)
$html = $this->rich_text_to_html( $rich_text );$color_class = $this->get_color_class( 'blue' );
// Returns: 'has-blue-color'$wp_color = $this->map_color( 'blue_background' );
// Returns: '#CCE4F9'$html = $this->wrap_gutenberg_block(
'core/paragraph',
'<p>Content</p>',
[ 'className' => 'my-class' ]
);
// Returns: <!-- wp:core/paragraph {"className":"my-class"} -->
// <p>Content</p>
// <!-- /wp:core/paragraph -->The following Notion block types support child blocks:
- Bulleted list item
- Callout
- Heading (when
is_toggleable= true) - Numbered list item
- Paragraph
- Quote
- Toggle
- To do
- Table
Always check for $block['children'] and process them recursively.
Problem: Notion returns each list item (bulleted, numbered, to-do) as a separate block, but Gutenberg expects consecutive list items to be grouped in a single list block.
Solution: The Block_Registry::group_associated_items() method automatically groups consecutive list items before conversion.
- Bulleted lists (
bulleted_list_item) - Numbered lists (
numbered_list_item) - To-do lists (
to_do)
-
Detection Phase -
Block_Registry::convert_blocks()callsgroup_associated_items()to scan for consecutive list items -
Grouping Phase - Consecutive items of the same type are collected:
// Input: Individual Notion blocks [ { "type": "bulleted_list_item", "bulleted_list_item": {...} }, { "type": "bulleted_list_item", "bulleted_list_item": {...} }, { "type": "bulleted_list_item", "bulleted_list_item": {...} } ] // Output: Grouped list block { "type": "bulleted_list_item", "is_grouped": true, "list_items": [ { "type": "bulleted_list_item", ... }, { "type": "bulleted_list_item", ... }, { "type": "bulleted_list_item", ... } ] }
-
Conversion Phase -
List_Converterdetects grouped lists and creates a single<ul>or<ol>with multiple<li>elements:<!-- Gutenberg: core/list --> <ul> <li>First item</li> <li>Second item</li> <li>Third item</li> </ul>
-
Nested Lists - Individual list items with children are handled separately (not grouped), allowing for proper nesting:
<ul> <li>Parent item <ul> <li>Nested item 1</li> <li>Nested item 2</li> </ul> </li> </ul>
Notion API Response:
[
{ "type": "bulleted_list_item", "bulleted_list_item": { "rich_text": [{"text": {"content": "Item 1"}}] } },
{ "type": "bulleted_list_item", "bulleted_list_item": { "rich_text": [{"text": {"content": "Item 2"}}], "children": [...] } },
{ "type": "bulleted_list_item", "bulleted_list_item": { "rich_text": [{"text": {"content": "Item 3"}}] } },
{ "type": "paragraph", "paragraph": { "rich_text": [...] } },
{ "type": "numbered_list_item", "numbered_list_item": { "rich_text": [{"text": {"content": "Numbered 1"}}] } },
{ "type": "numbered_list_item", "numbered_list_item": { "rich_text": [{"text": {"content": "Numbered 2"}}] } },
{ "type": "to_do", "to_do": { "rich_text": [{"text": {"content": "Task 1"}}], "checked": false } },
{ "type": "to_do", "to_do": { "rich_text": [{"text": {"content": "Task 2"}}], "checked": true } }
]Converted Output:
<!-- core/list (bulleted) -->
<ul>
<li>Item 1</li>
<li>Item 2
<!-- Nested children here -->
</li>
<li>Item 3</li>
</ul>
<!-- /core/list -->
<!-- core/paragraph -->
<p>Paragraph text</p>
<!-- /core/paragraph -->
<!-- core/list (numbered) -->
<ol>
<li>Numbered 1</li>
<li>Numbered 2</li>
</ol>
<!-- /core/list -->
<!-- core/list (to-do) -->
<ul>
<li>☐ Task 1</li>
<li>☑ Task 2</li>
</ul>
<!-- /core/list -->
<li>Numbered 2</li>
</ol>
<!-- /core/list -->- Create a test Notion page with the specific block type
- Use the Import UI to import the page
- Verify the WordPress post contains the correct Gutenberg blocks
- Check the post in the block editor to ensure proper rendering
- Keep converters simple - Each converter should handle one block type
- Use utility methods - Leverage the base class methods for common tasks
- Handle edge cases - Check for empty content, missing properties, etc.
- Preserve formatting - Use
rich_text_to_html()to maintain text styles - Process children - Always handle nested blocks when supported
- Escape output - Use WordPress escaping functions (
esc_html(),esc_url(), etc.) - Add attributes - Include relevant Gutenberg block attributes for better editing
Potential improvements to the block converter system:
- Media Download - Automatically download and import images/files to WordPress media library
- Advanced Block Mapping - Map Notion blocks to custom Gutenberg blocks
- Conversion Settings - Allow users to configure conversion preferences
- Block Caching - Cache converted blocks for performance
- Conversion Hooks - Add filters/actions for customization
- Column Layouts - Support for Notion's column_list and column blocks
- Synced Blocks - Handle Notion's synced block feature
- Equation Blocks - Support for mathematical equations (KaTeX)
- PDF Blocks - Embed PDF files
- Breadcrumb Blocks - Navigation breadcrumbs
- Check if converter is registered in
Block_Registry - Verify
supports()method returns true for the block - Add error logging in
convert()method - Check for PHP errors in debug log
- Ensure
process_children()is called - Verify
has_childrenproperty is true in Notion block - Check if child blocks are fetched by
Notion_Client
- Use
rich_text_to_html()instead ofextract_plain_text() - Check annotation handling in base class
- Verify escaping isn't stripping HTML tags