Skip to content
beginner Phase 116 · Beginner Projects

Project: Create a Custom CMS Page

Build a custom CMS page with blocks, templates, and layout configuration

45m
2 problems
Topic Progress 0%

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

0 / 2 solved
Custom CMS Page

Create a custom CMS page with header, content area, and sidebar widget.

Reusable Widget

Build a reusable widget block that displays recent CMS pages.

Quiz

1. What defines a custom route in Magento?

Question 1 options

2. How to add CSS to a custom page?

Question 2 options

3. What is a layout handle?

Question 3 options

4. How to make a block a widget?

Question 4 options

Flashcards

Question

What defines routes?

Answer

routes.xml maps URLs to controllers

Question

How to add CSS in layout?

Answer

<head><css src="path"/></head> in layout XML

Question

What is a layout handle?

Answer

Identifier triggering specific layout for pages/routes

Question

How to create a widget?

Answer

Implement BlockInterface and define in widget.xml

Question

How to create CMS content?

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: