Module Setup for CMS
Module Structure
Vendor/CustomCmsPage/
├── etc/
│ ├── module.xml
│ ├── registration.php
│ ├── routes.xml
│ └── di.xml
├── Block/
│ ├── CmsPage.php
│ └── Widget/
│ └── RecentPosts.php
├── Controller/
│ └── Page
│ └── View.php
├── view/
│ ├── frontend/
│ │ ├── layout/
│ │ │ ├── custom_cms_page_view.xml
│ │ │ ├── cms_page_view.xml
│ │ │ └── default.xml
│ │ ├── templates/
│ │ │ ├── cms-page.phtml
│ │ │ └── widget/
│ │ │ └── recent-posts.phtml
│ │ └── web/
│ │ ├── css/
│ │ │ └── custom-cms.css
│ │ └── js/
│ │ └── custom-cms.js
│ └── adminhtml/
├── i18n/
│ └── en_US.csv
└── composer.json
routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="customcmspage" frontName="customcmspage">
<module name="Vendor_CustomCmsPage"/>
</route>
</router>
</config>
Controller
<?php
namespace Vendor\CustomCmsPage\Controller\Page;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
class View extends Action
{
protected $resultPageFactory;
public function __construct(
Context $context,
PageFactory $resultPageFactory
) {
parent::__construct($context);
$this->resultPageFactory = $resultPageFactory;
}
public function execute()
{
$page = $this->resultPageFactory->create();
$page->getConfig()->getTitle()->set(__('Custom CMS Page'));
return $page;
}
}
Access URL
https://example.com/customcmspage/page/view
Layout Configuration
Page Layout
Default Layout
<!-- view/frontend/layout/default.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<css src="Vendor_CustomCmsPage::css/custom-cms.css"/>
</head>
</page>
Custom Page Layout
<!-- view/frontend/layout/custom_cms_page_view.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"
layout="2columns-left">
<update handle="customcmspage_cms_page_view"/>
<body>
<referenceContainer name="content">
<block class="Vendor\CustomCmsPage\Block\CmsPage"
name="custom.cms.page"
template="Vendor_CustomCmsPage::cms-page.phtml"/>
</referenceContainer>
<referenceContainer name="sidebar.main">
<block class="Vendor\CustomCmsPage\Block\Widget\RecentPosts"
name="custom.recent.posts"
template="Vendor_CustomCmsPage::widget/recent-posts.phtml"/>
</referenceContainer>
</body>
</page>
CMS Page Layout
<!-- view/frontend/layout/cms_page_view.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Magento\Cms\Block\Page" name="cms_page">
<arguments>
<argument name="page_id" xsi:type="query">id</argument>
</arguments>
</block>
</referenceContainer>
</body>
</page>
Layout Handles
Page-Specific Handle
<!-- For specific CMS page ID 5 -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<update handle="cms_page_view_id_5"/>
</page>
Route-Based Handle
<!-- Automatic handle: customcmspage_page_view -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<update handle="customcmspage_page_view"/>
</page>
Container Layout
<page layout="1column">
<body>
<!-- Full width container -->
<referenceContainer name="content">
<container name="custom.wrapper" htmlTag="div" htmlClass="custom-wrapper">
<container name="custom.header" htmlTag="div" htmlClass="custom-header">
<block class="Magento\Framework\View\Element\Text" name="custom.title">
<arguments>
<argument name="text" xsi:type="string">Custom Page</argument>
</arguments>
</block>
</container>
<container name="custom.body" htmlTag="div" htmlClass="custom-body"/>
</container>
</referenceContainer>
</body>
</page>
Blocks and Templates
CMS Block
<?php
namespace Vendor\CustomCmsPage\Block;
use Magento\Framework\View\Element\Template;
use Magento\Cms\Model\BlockFactory;
use Magento\Cms\Model\BlockRepository;
class CmsPage extends Template
{
public function __construct(
Template\Context $context,
private BlockFactory $blockFactory,
private BlockRepository $blockRepository,
array $data = []
) {
parent::__construct($context, $data);
}
public function getCmsBlock($identifier)
{
try {
$block = $this->blockRepository->getByIdentifier($identifier);
return $block->getContent();
} catch (\Exception $e) {
return '';
}
}
public function getPageTitle()
{
return $this->getData('page_title') ?? __('Custom Page');
}
public function getWelcomeMessage()
{
return __('Welcome to our custom page!');
}
}
CMS Template
<!-- view/frontend/templates/cms-page.phtml -->
<?php
/** @var \Vendor\CustomCmsPage\Block\CmsPage $block */
?>
<div class="custom-cms-page">
<div class="page-header">
<h1><?= $block->escapeHtml($block->getPageTitle()) ?></h1>
<p class="welcome-message"><?= $block->escapeHtml($block->getWelcomeMessage()) ?></p>
</div>
<div class="page-content">
<?= $block->getCmsBlock('about-us-content') ?>
</div>
<div class="page-features">
<div class="feature">
<h3><?= __('Feature 1') ?></h3>
<p><?= __('Description of feature 1') ?></p>
</div>
<div class="feature">
<h3><?= __('Feature 2') ?></h3>
<p><?= __('Description of feature 2') ?></p>
</div>
<div class="feature">
<h3><?= __('Feature 3') ?></h3>
<p><?= __('Description of feature 3') ?></p>
</div>
</div>
</div>
Widget Block
<?php
namespace Vendor\CustomCmsPage\Block\Widget;
use Magento\Framework\View\Element\Template;
use Magento\Widget\Block\BlockInterface;
use Magento\Cms\Model\ResourceModel\Page\CollectionFactory;
class RecentPosts extends Template implements BlockInterface
{
protected $_template = 'widget/recent-posts.phtml';
public function __construct(
Template\Context $context,
private CollectionFactory $pageCollectionFactory,
array $data = []
) {
parent::__construct($context, $data);
}
public function getRecentPages($count = 5)
{
$collection = $this->pageCollectionFactory->create();
$collection->addFieldToFilter('is_active', 1)
->setOrder('creation_time', 'DESC')
->setPageSize($count);
return $collection;
}
}
Widget Template
<!-- view/frontend/templates/widget/recent-posts.phtml -->
<?php
/** @var \Vendor\CustomCmsPage\Block\Widget\RecentPosts $block */
$pages = $block->getRecentPages();
?>
<div class="widget-recent-posts">
<h3><?= __('Recent Posts') ?></h3>
<ul>
<?php foreach ($pages as $page): ?>
<li>
<a href="<?= $block->escapeUrl($block->getUrl('cms/page/view', ['id' => $page->getId()])) ?>">
<?= $block->escapeHtml($page->getTitle()) ?>
</a>
<span class="date">
<?= $block->formatDate($page->getCreationTime()) ?>
</span>
</li>
<?php endforeach; ?>
</ul>
</div>
CSS
/* view/frontend/web/css/custom-cms.css */
.custom-cms-page {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-header {
text-align: center;
margin-bottom: 40px;
}
.page-header h1 {
font-size: 2.5rem;
color: #333;
}
.page-features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
margin-top: 40px;
}
.feature {
background: #f9f9f9;
padding: 30px;
border-radius: 8px;
text-align: center;
}
.feature h3 {
color: #1979c3;
margin-bottom: 15px;
}
.widget-recent-posts {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
}
.widget-recent-posts h3 {
margin-bottom: 15px;
}
.widget-recent-posts ul {
list-style: none;
padding: 0;
}
.widget-recent-posts li {
margin-bottom: 10px;
}
.widget-recent-posts .date {
display: block;
font-size: 0.85rem;
color: #666;
}
Deployment and Configuration
Install Module
php bin/magento module:enable Vendor_CustomCmsPage
php bin/magento setup:upgrade
php bin/magento cache:clean
php bin/magento setup:static-content:deploy -f
Admin Configuration
Create CMS Block
1. Go to Content > Elements > Blocks
2. Click "Add New Block"
3. Fill in:
- Block Title: About Us Content
- Identifier: about-us-content
- Content: [HTML content]
4. Save Block
Create CMS Page
1. Go to Content > Elements > Pages
2. Click "Add New Page"
3. Fill in:
- Page Title: About Us
n - URL Key: about-us
- Content: [HTML or widget]
- Design: 2columns-left
4. Save Page
Add Widget
1. Go to Content > Elements > Widgets
2. Click "Add Widget"
3. Fill in:
- Type: Custom Recent Posts
- Design Package: your theme
4. Configure widget instances
5. Save Widget
Page Assignments
<!-- Assign layout to specific pages -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"
layout="2columns-left">
<update handle="customcmspage_cms_page_view"/>
</page>
Test Page
1. Navigate to your custom page URL
2. Verify layout renders correctly
3. Check blocks display content
4. Test widget functionality
5. Verify CSS loads
6. Check responsive design
Troubleshooting
- Page not found: Check routes.xml and controller
- Layout not applying: Verify layout handle name
- Block not rendering: Check block class and template path
- Content not showing: Verify CMS block identifier
- CSS not loading: Check CSS file path in layout
Practice Problems
Create a custom CMS page with header, content area, and sidebar widget.
Build a reusable widget block that displays recent CMS pages.
Quiz
1. What defines a custom route in Magento?
2. How to add CSS to a custom page?
3. What is a layout handle?
4. How to make a block a widget?
Flashcards
Question
What defines routes?
Click to reveal answer
Answer
routes.xml maps URLs to controllers
Question
How to add CSS in layout?
Click to reveal answer
Answer
<head><css src="path"/></head> in layout XML
Question
What is a layout handle?
Click to reveal answer
Answer
Identifier triggering specific layout for pages/routes
Question
How to create a widget?
Click to reveal answer
Answer
Implement BlockInterface and define in widget.xml
Question
How to create CMS content?
Click to reveal answer
Answer
Content > Elements > Blocks/Pages in admin
Revision Notes
Key Takeaways
- 1. routes.xml defines custom routes for controllers
- 2. Layout XML uses handles to apply page-specific layouts
- 3. Blocks provide data to templates, Templates render HTML
- 4. Widgets implement BlockInterface for admin-configurable blocks
- 5. CSS/JS added via layout XML head section
- 6. CMS content managed through admin: Blocks and Pages
Interview Tips
- • How do you create a custom CMS page?
- • Explain layout handles and how they work
- • How do you add custom CSS/JS to a page?
- • What is a widget and how do you create one?
- • How do you make content editable through admin?
Cheat Sheet
Custom CMS Page Cheat Sheet
Files:
- routes.xml: URL routing
- layout/*.xml: page structure
- Block/: data provider
- templates/: HTML output
Layout:
- default.xml: all pages
- custom_page.xml: specific page
- Handles: page-specific overrides
Widgets:
- Implement BlockInterface
- Define in widget.xml
- Configurable in admin
CSS/JS:
Admin:
- Content > Elements > Blocks
- Content > Elements > Pages
- Content > Elements > Widgets