Skip to content

Architecture

app/code/OrangeCollar/WordPressIntegration/
├── Controller/
│ ├── Router.php # Custom router (sortOrder 25)
│ ├── Blog/Index.php # Blog listing
│ ├── Post/View.php # Single post
│ ├── Category/View.php # Category archive
│ ├── Tag/View.php # Tag archive
│ ├── Author/View.php # Author archive
│ ├── Search/Results.php # Search results
│ └── Webhook/CachePurge.php # Webhook receiver
├── Model/
│ ├── Api/
│ │ ├── ClientInterface.php # API client interface
│ │ ├── Client.php # Guzzle implementation
│ │ └── Authentication.php # Header auth helper
│ ├── Cache/
│ │ ├── WordPressCache.php # Cache wrapper
│ │ └── CacheTag.php # Cache tag constants
│ ├── Repository/
│ │ ├── PostRepository.php
│ │ ├── CategoryRepository.php
│ │ ├── TagRepository.php
│ │ └── MenuRepository.php
│ ├── Config.php # All config access
│ ├── ContentProcessor.php # HTML processing pipeline
│ ├── Post.php # Post data model
│ ├── Category.php # Category data model
│ ├── Tag.php # Tag data model
│ └── Menu.php # Menu data model
├── Plugin/
│ ├── SeoPlugin.php # Injects WP SEO meta
│ └── BreadcrumbPlugin.php # Blog breadcrumbs
├── Observer/
│ └── LayoutLoadBefore.php # Adds layout handles
├── ViewModel/
│ ├── Post.php
│ ├── PostList.php
│ └── Breadcrumbs.php
├── Block/
│ ├── Widget/
│ │ ├── RecentPosts.php
│ │ ├── FeaturedPost.php
│ │ └── CategoryPosts.php
│ └── Sidebar/
│ ├── Categories.php
│ ├── Tags.php
│ ├── Archive.php
│ ├── Search.php
│ └── RecentPosts.php
├── Cron/
│ └── WarmCache.php
├── etc/
│ ├── di.xml # Global DI preferences
│ ├── frontend/di.xml # Frontend router registration
│ ├── adminhtml/system.xml # Admin config fields
│ ├── config.xml # Default config values
│ ├── crontab.xml # Cron schedules
│ ├── widget.xml # Widget declarations
│ └── events.xml # Event observers
└── view/frontend/
├── layout/ # Layout XML per handle
└── templates/ # Phtml templates
<!-- API client preference -->
<preference for="OrangeCollar\WordPressIntegration\Model\Api\ClientInterface"
type="OrangeCollar\WordPressIntegration\Model\Api\Client" />
<!-- Repository preference -->
<preference for="OrangeCollar\WordPressIntegration\Model\Repository\PostRepositoryInterface"
type="OrangeCollar\WordPressIntegration\Model\Repository\PostRepository" />
<!-- Virtual logger -->
<virtualType name="OrangeCollar\WordPressIntegration\Logger\Handler"
type="Magento\Framework\Logger\Handler\Base">
<arguments>
<argument name="fileName" xsi:type="string">/var/log/orangecollar_wordpress.log</argument>
</arguments>
</virtualType>

The custom router is registered here - it must be in the frontend scope DI config:

<type name="Magento\Framework\App\RouterList">
<arguments>
<argument name="routerList" xsi:type="array">
<item name="wordpress" xsi:type="array">
<item name="class" xsi:type="string">OrangeCollar\WordPressIntegration\Controller\Router</item>
<item name="disable" xsi:type="boolean">false</item>
<item name="sortOrder" xsi:type="string">25</item>
</item>
</argument>
</arguments>
</type>
  1. Browser requests /blog/my-post-slug
  2. Magento’s router list runs in sortOrder
  3. At sortOrder 25, Router::match() checks if the URL starts with the configured base path
  4. Router parses the path and sets module, controller, action, and params on the request
  5. Magento dispatches to the resolved controller (e.g., Post\View)
  6. LayoutLoadBefore observer adds the layout handle (e.g., orangecollar_post_view)
  7. Controller fetches data via the repository, which calls the API client and manages caching
  8. Blocks and ViewModels render the content via layout XML and templates

The LayoutLoadBefore observer listens to the layout_load_before event and adds layout handles dynamically:

// Generates handles like: orangecollar_post_view, orangecollar_blog_index
$handle = 'orangecollar_' . $moduleName . '_' . $actionName;
$layout->getUpdate()->addHandle($handle);

This allows per-page-type layout customization without controller inheritance complexity.

All data models are immutable value objects:

final class Post
{
public function __construct(
public readonly int $id,
public readonly string $slug,
public readonly string $title,
// ... other properties
) {}
public static function fromApiResponse(array $data): self
{
return new self(
id: $data['id'],
slug: $data['slug'],
title: $data['title']['rendered'],
// ... map other fields
);
}
}

The fromApiResponse() factory handles the WP REST API response structure, including embedded author and term data from _embedded.