Sylius CMS Plugin — How to bulid a blog in Sylius — BitBag
Sylius by default does not offer any CMS-typical capabilities nor does it have the ability to build a blog in a quick way; fortunately, our CMS plugin comes to the rescue.
This guide will be more backend-oriented and focused on configuration, but I will provide some design examples too.
SyliusCMSPlugin will be used in the way of:
BitBagCMSPlugin can be found in the Sylius store, but if you need more information, please !
Getting started
To build a blog, we need two things: Sylius shop and our CMS Plugin (you can click here for the plugin installation guide). Once you install the plugin, update the database schema and build assets from the plugin — you should see a new content management category at the bottom of the menu in the administration panel:
Grid declaration
At the beginning we need to declare a grid that will support displaying the list of our blog posts, in the Sylius we can declare a grid for example in the config/packages/_sylius.yaml file like this:
sylius_grid:
grids:
bitbag_sylius_cms_plugin_shop_page:
driver:
name: doctrine/orm
options:
class: "%bitbag_sylius_cms_plugin.model.page.class%"
repository:
method: createShopListQueryBuilder
arguments:
sectionCode: "blog"
channelCode: expr:service('sylius.context.channel').getChannel().getCode()
sorting:
createdAt: desc
fields:
createdAt:
type: datetime
sortable: ~
limits: [9]
This is how we declared a grid named bitbag_sylius_cms_plugin_shop_page that uses the Page repository from CMS Plugin. Grid in the repository calls the createShopListQueryBuilder function, passing to it the sectionCode “blog” defined in the grid and the Sylius ChannelContext class from which it extracts the code of the current channel. At the end of the grid declaration, we can see the sorting of the results by the page creation date.
Routing declaration
In the next step we need to declare the routing for our blog, for the main page of the blog we can do it like this:
app_shop_blog:
path: /blogposts
methods: [GET]
defaults:
sectionCode: blog
_controller: bitbag_sylius_cms_plugin.controller.page::indexAction
_sylius:
template: "app/Blog/index.html.twig"
grid: bitbag_sylius_cms_plugin_shop_page
And for blog post pages we can do it like this:
app_shop_blog_post_show:
path: /blogpost/{slug}
methods: [GET]
defaults:
_controller: bitbag_sylius_cms_plugin.controller.page::showAction
_sylius:
template: "app/Blog/show.html.twig"
repository:
method: findOneEnabledBySlugAndChannelCode
arguments:
- $slug
- "expr:service('sylius.context.locale').getLocaleCode()"
- "expr:service('sylius.context.channel').getChannel().getCode()"
requirements:
slug: .+
In the declaration of both routes, we use the PageController class from the CMS plugin which extends the ResourceController. We need to create two templates — for example the index.html.twig and show.html.twig, we will deal with their content in the next steps. In the routing responsible for displaying the blog post, we use PageRepository again, to which we must pass the slug of our blog post.
Creating templates
As I mentioned earlier — we need to define two templates that we use in routing. We will start with the template responsible for displaying the list of blog posts. Below is a very simple example that can be adapted to your requirements:
{#app/Blog/index.html.twig#}
{% extends '@SyliusShop/layout.html.twig' %}
{% block content %}
{% for page in resources.data %}
<div class="col-12 col-md-6 col-lg-4">
<div>
{% if page.image %}
<a href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
<img src="{{ page.image.path|imagine_filter(filter|default('sylius_large')) }}"/>
</a>
{% else %}
<a href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
<div style="background-image: url(https://placehold.co/400x300)"></div>
</a>
{% endif %}
<header>
<a href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
<h2>
{{ page.name }}
</h2>
</a>
</header>
<div>
<p>
{{ page.descriptionWhenLinked }}
</p>
<a href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
{{ 'app.blog.more' | trans }}
</a>
<p>
{{ page.createdAt | date('Y-m-d') }}
</p>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
Short explanation: with a for loop, we go through all our pages that have blog sections assigned, and we display a thumbnail of each blog post along with its main photo, name, description, and creation date.
Now we need a template to display a specific blog post, and we can do it for example like this:
{#app/Blog/show.html.twig#}
{% extends '@SyliusShop/layout.html.twig' %}
{% block content %}
<article>
<h1>{{ page.name }}</h1>
{% if page.image %}
<div>
<div class="col-12">
<img src="{{ page.image.path }}" alt="{{ page.name }}">
</div>
</div>
<div class="ui hidden divider"></div>
<div class="ui hidden divider"></div>
{% endif %}
<div class="row justify-content-center">
<div class="col-12 col-lg-3 col-xl-4"></div>
<div class="col-12 col-lg-9 col-xl-8">
{{ bitbag_cms_render_content(page) }}
</div>
</div>
</article>
{% endblock %}
And we’re not doing anything special here either, other than displaying an image of the article and rendering its content using a twig extension called bitbag_cms_render_content.
Creating section and posts
The next thing we need to do is to create a new section — you can create that in your admin panel in the Sections menu in the Content Management category. You can set the code of this section as “blog” or you can name whatever you want (but if you use the other code than the blog, please remember to set the proper section code in the grid and routes definition).
And we can create our first blog post in the pages tab in the administration panel. When you create the page, you have to select the blog section, and you can set the rest of the values as you want.
Once you’ve successfully created your page, you can go to /the blog posts route and you should see your first blogpost — the design isn’t very impressive but we’ll come back to that in a moment.
Pagination
When we have only a few posts on the blog, we don’t have any problem, but when we have a dozen of them, some pagination would be useful. At the bottom of our index.html.twig file we can import the default pagination from Sylius like the code below:
...
{% endfor %}
<div class="row">
<div class="col-12">
{% include '@SyliusShop/Product/Index/_pagination.html.twig' with {'data': resources.data} %}
</div>
</div>
..but the effect is rather average:
So we can slightly improve it by overwriting _pagination.html.twig for example like this..
{#templates/bundles/SyliusShopBundle/Product/Index/_pagination.html.twig#}
{% set route = app.request.attributes.get('_route') %}
{% set routeParameters = app.request.attributes.get('_route_params') %}
{% if data.nbResults <= data.maxPerPage %}
{% set numberOfPages = 1 %}
{% else %}
{% set numberOfPages = (data.nbResults / data.maxPerPage)|round(0, 'ceil') %}
{% endif %}
<div class="product-list-pagination">
{% if data.currentPage > 1 %}
<a class="product-list-pagination__arrow" aria-label="{{ 'app.ui.previous_page'|trans }}" href="{{ path(route, routeParameters|merge(app.request.query.all)|merge({'page': '1'})) }}">
<img class="shop-header__menu-actions-icon" alt="prev" src="{{ asset('build/app/shop/images/angles-left-thin.svg', 'app.shop') }}"/>
</a>
<a class="product-list-pagination__arrow" aria-label="{{ 'app.ui.previous_page'|trans }}" href="{{ path(route, routeParameters|merge(app.request.query.all)|merge({'page': data.currentPage - 1})) }}">
<img class="shop-header__menu-actions-icon" alt="prev" src="{{ asset('build/app/shop/images/angle-left-thin.svg', 'app.shop') }}"/>
</a>
{% else %}
<span class="product-list-pagination__arrow product-list-pagination__arrow--disabled">
<img class="shop-header__menu-actions-icon shop-header__menu-actions-icon--disabled" alt="prev" src="{{ asset('build/app/shop/images/angles-left-thin.svg', 'app.shop') }}"/>
</span>
<span class="product-list-pagination__arrow product-list-pagination__arrow--disabled">
<img class="shop-header__menu-actions-icon shop-header__menu-actions-icon--disabled" alt="prev" src="{{ asset('build/app/shop/images/angle-left-thin.svg', 'app.shop') }}"/>
</span>
{% endif %}
<span class="product-list-pagination__page">{{ 'product_list.pagination.page'|trans({'{currentPage}': data.currentPage, '{maxPages}': numberOfPages}) }}</span>
{% if data.currentPage < numberOfPages %}
<a class="product-list-pagination__arrow" aria-label="{{ 'app.ui.next_page'|trans }}" href="{{ path(route, routeParameters|merge(app.request.query.all)|merge({'page': data.currentPage + 1})) }}">
<img class="shop-header__menu-actions-icon" alt="prev" src="{{ asset('build/app/shop/images/angle-right-thin.svg', 'app.shop') }}"/>
</a>
<a class="product-list-pagination__arrow" aria-label="{{ 'app.ui.next_page'|trans }}" href="{{ path(route, routeParameters|merge(app.request.query.all)|merge({'page': numberOfPages})) }}">
<img class="shop-header__menu-actions-icon" alt="prev" src="{{ asset('build/app/shop/images/angles-right-thin.svg', 'app.shop') }}"/>
</a>
{% else %}
<span class="product-list-pagination__arrow product-list-pagination__arrow--disabled">
<img class="shop-header__menu-actions-icon shop-header__menu-actions-icon--disabled" alt="prev" src="{{ asset('build/app/shop/images/angle-right-thin.svg', 'app.shop') }}"/>
</span>
<span class="product-list-pagination__arrow product-list-pagination__arrow--disabled">
<img class="shop-header__menu-actions-icon shop-header__menu-actions-icon--disabled" alt="prev" src="{{ asset('build/app/shop/images/angles-right-thin.svg', 'app.shop') }}"/>
</span>
{% endif %}
</div>
..with a light dose of CSS..
product-list-pagination {
display: flex;
justify-content: center;
align-items: center;
}
.product-list-pagination__page {
margin: 0 20px;
color: #1e1e1e;
}
.product-list-pagination__arrow {
color: #1e1e1e;
margin: 0 4px;
&:hover {
text-decoration: none;
color: #313131;
}
}
.product-list-pagination__arrow--disabled {
color: #a7a7a7;
&:hover {
color: #a7a7a7;
}
}
.shop-header__menu-actions-icon {
display: inline-block;
vertical-align: middle;
height: 15px;
margin-bottom: 3px;
}
.shop-header__menu-actions-icon--disabled {
opacity: 0.3;
pointer-events: none;
}
..and after some icons import we have our pagination (which works properly and looks much better)!
Some blog styling
After completing all the previous steps, we can proceed to a slight change in the design of the blog — we can start with adding some div classes in the index.html.twig, for example, like this:
{#app/Blog/index.html.twig#}
{% extends '@SyliusShop/layout.html.twig' %}
{% block content %}
<section class="container">
<div class="row">
<div class="col-12 col-lg-12">
<div class="hero-image" style="background-image: url({{ asset('build/app/shop/images/blog-background.jpg', 'app.shop') }})"></div>
</div>
</div>
<div class="row">
<div class="col-12 col-lg-6"></div>
<div class="col-12 col-lg-6">
<section class="text-section">
<p class="blog-page__intro">
{{ 'blog.intro'|trans }}
</p>
</section>
</div>
</div>
<div class="row justify-content-center">
<div class="col-12">
<section class="text-section">
<h1 class="text-section__title text-section__title--blog">
<span>{{ 'blog.title_first_part'|trans }}</span>
<span class="text-section__highlight">{{ 'blog.title_second_part'|trans }}</span>
</h1>
</section>
</div>
</div>
<div class="row">
{% for page in resources.data %}
<div class="col-12 col-md-6 col-lg-4">
<div class="blog-page__post">
{% if page.image %}
<a class="blog-page__post-image-link" href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
<div class="blog-page__post-image" style="background-image: url({{ page.image.path|imagine_filter(filter|default('sylius_large')) }})"></div>
</a>
{% else %}
<a class="blog-page__post-image-link" href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
<div class="blog-page__post-image" style="background-image: url(https://placehold.co/400x300)"></div>
</a>
{% endif %}
<header class="blog-page__post-header">
<a class="blog-page__post-title-link" href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
<h2 class="blog-page__post-title">
{{ page.name }}
</h2>
</a>
</header>
<div class="blog-page__post-content">
<p class="blog-page__post-description">
{{ page.descriptionWhenLinked }}
</p>
<a class="blog-page__post-see-more text-uppercase" href="{{ path('app_shop_blog_post_show', {'slug' : page.slug}) }}">
{{ 'blog.more' | trans }}
</a>
<p class="blog-page__post-date">
{{ page.createdAt | date('Y-m-d') }}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
<div class="row">
<div class="col-12">
{% include '@SyliusShop/Product/Index/_pagination.html.twig' with {'data': resources.data} %}
</div>
</div>
{% endblock %}
And some css styling..
.blog-page__post {
margin-bottom: 36px;
}
.blog-page__post-image {
height: 286px;
background-size: cover;
background-position: center center;
}
.blog-page__blog-link,
.blog-page__post-title-link:hover,
.blog-page__post-title,
.blog-page__post-see-more,
.blog-page__post-see-more:hover {
color: $color-base-900;
text-decoration: none;
}
.blog-page__blog-link,
.blog-page__post-see-more {
font-size: 16px;
line-height: 2.2;
}
.blog-page__post-date,
.blog-page__blog-link,
.blog-page__post-header {
margin-bottom: 8px;
}
.blog-page__post-title {
font-size: 18px;
font-weight: 300;
margin-top: 12px !important;
font-family: "Montserrat", sans-serif;
}
.blog-page__post-see-more {
margin-bottom: 20px;
}
.blog-page__post-date {
padding: 10px 0;
border-top: 1px solid $color-base-700;
}
.blog-page__intro {
padding-top: 70px;
font-size: 16px;
margin-bottom: 16px;
font-weight: 300;
font-family: "Montserrat", sans-serif;
padding-right: 10%;
}
The end result might look like this:
Summary
This guide has provided a comprehensive walkthrough of building a blog using the BitBagCMSPlugin within the Sylius platform. By setting up the plugin, declaring grids and routes, creating templates, and styling the blog, you can create an effective and aesthetically pleasing blog that seamlessly integrates with Sylius. With this step-by-step approach, you can enhance your Sylius-based website by incorporating a fully functional and stylish blog.
And don’t forget to add your own designs to the display page of a specific blog post!
Originally published at https://bitbag.io on August 24, 2023.