Development of Custom Templates in Craft CMS (Twig)
Craft CMS uses Twig 3 with additional filters, functions and global variables. Templates are resolved by URI: request /blog/my-post looks for templates templates/blog/my-post.twig, templates/blog/_entry.twig, templates/blog.twig in order.
Template Structure
templates/
├── _layouts/
│ ├── base.twig # base HTML skeleton
│ ├── default.twig # standard layout
│ └── fullwidth.twig # without sidebar
├── _components/
│ ├── header.twig
│ ├── footer.twig
│ ├── post-card.twig
│ └── breadcrumbs.twig
├── _macros/
│ └── helpers.twig # reusable Twig functions
├── index.twig # home page
├── 404.twig # error page
├── blog/
│ ├── index.twig # post list
│ └── _entry.twig # single post
└── services/
├── index.twig
└── _entry.twig
Base Layout
{# templates/_layouts/base.twig #}
<!DOCTYPE html>
<html lang="{{ craft.app.language }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ siteName }}{% endblock %}</title>
{% block meta %}{% endblock %}
{{ craft.vite.stylesheet('src/css/app.pcss') }}
</head>
<body class="{% block bodyClass %}{% endblock %}">
{% include '_components/header' %}
<main id="main" tabindex="-1">
{% block content %}{% endblock %}
</main>
{% include '_components/footer' %}
{{ craft.vite.script('src/js/app.ts') }}
{% block scripts %}{% endblock %}
</body>
</html>
Entry List Template
{# templates/blog/index.twig #}
{% extends '_layouts/default' %}
{% set pageInfo = craft.app.request.getQueryParam('page') %}
{% block content %}
{% paginate craft.entries()
.section('blog')
.status('live')
.orderBy('postDate desc')
.limit(12) as pageInfo, posts %}
<div class="blog-grid">
{% for post in posts %}
{% include '_components/post-card' with { post: post } only %}
{% else %}
<p>No posts yet</p>
{% endfor %}
</div>
{% if pageInfo.totalPages > 1 %}
<nav class="pagination">
{% if pageInfo.prevUrl %}
<a href="{{ pageInfo.prevUrl }}" rel="prev">← Previous</a>
{% endif %}
{% for page, url in pageInfo.getPrevUrls(3) %}
<a href="{{ url }}">{{ page }}</a>
{% endfor %}
<span class="current">{{ pageInfo.currentPage }}</span>
{% for page, url in pageInfo.getNextUrls(3) %}
<a href="{{ url }}">{{ page }}</a>
{% endfor %}
{% if pageInfo.nextUrl %}
<a href="{{ pageInfo.nextUrl }}" rel="next">Next →</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}
Entry Template with Matrix
{# templates/blog/_entry.twig #}
{% extends '_layouts/default' %}
{% set post = entry %}
{% block title %}{{ post.title }} | {{ siteName }}{% endblock %}
{% block meta %}
<meta name="description" content="{{ post.seoDescription ?? post.summary | striptags | slice(0, 160) }}">
<meta property="og:title" content="{{ post.title }}">
<meta property="og:image" content="{{ post.heroImage.one().getUrl('ogImage') ?? '' }}">
{% endblock %}
{% block content %}
<article class="post">
<header>
{% set heroImage = post.heroImage.one() %}
{% if heroImage %}
<picture>
<source
srcset="{{ heroImage.getUrl({ width: 800, format: 'webp' }) }} 800w,
{{ heroImage.getUrl({ width: 1600, format: 'webp' }) }} 1600w"
type="image/webp">
<img
src="{{ heroImage.getUrl('large') }}"
alt="{{ heroImage.alt ?? post.title }}"
width="{{ heroImage.width }}"
height="{{ heroImage.height }}"
fetchpriority="high">
</picture>
{% endif %}
<div class="post-meta">
{% for category in post.categories.all() %}
<a href="{{ category.url }}" class="category">{{ category.title }}</a>
{% endfor %}
</div>
<h1>{{ post.title }}</h1>
<div class="byline">
{% set author = post.author %}
{% if author.photo.one() %}
<img src="{{ author.photo.one().getUrl('avatar') }}" alt="{{ author.fullName }}">
{% endif %}
<span>{{ author.fullName }}</span>
<time datetime="{{ post.postDate | date('Y-m-d') }}">
{{ post.postDate | date('d F Y') }}
</time>
</div>
</header>
<div class="post-body">
{% for block in post.pageContent.all() %}
{% switch block.type %}
{% case 'richText' %}
<div class="prose">{{ block.body }}</div>
{% case 'pullQuote' %}
<blockquote class="pull-quote">
<p>{{ block.quote }}</p>
{% if block.attribution %}<cite>{{ block.attribution }}</cite>{% endif %}
</blockquote>
{% case 'imageBlock' %}
{% set img = block.image.one() %}
{% if img %}
<figure class="image-block {{ block.size }}">
<img src="{{ img.getUrl({ width: 1200 }) }}" alt="{{ img.alt }}">
{% if block.caption %}<figcaption>{{ block.caption }}</figcaption>{% endif %}
</figure>
{% endif %}
{% case 'codeSnippet' %}
<pre><code class="language-{{ block.language }}">{{ block.code | escape }}</code></pre>
{% endswitch %}
{% endfor %}
</div>
{# Related posts via relationships #}
{% set related = craft.entries()
.section('blog')
.relatedTo(post.tags.all())
.id('not ' ~ post.id)
.limit(3)
.all() %}
{% if related | length %}
<aside class="related">
<h3>Read Also</h3>
{% for item in related %}
{% include '_components/post-card' with { post: item } only %}
{% endfor %}
</aside>
{% endif %}
</article>
{% endblock %}
Macros and Twig Extensions
{# templates/_macros/helpers.twig #}
{% macro truncate(text, length = 150) %}
{% if text | length > length %}
{{ text | slice(0, length) }}…
{% else %}
{{ text }}
{% endif %}
{% endmacro %}
{% macro breadcrumbs(entry) %}
<nav aria-label="Breadcrumbs">
<a href="/">Home</a>
{% for ancestor in entry.getAncestors() %}
<a href="{{ ancestor.url }}">{{ ancestor.title }}</a>
{% endfor %}
<span aria-current="page">{{ entry.title }}</span>
</nav>
{% endmacro %}
Named Image Transforms
// In Craft CP: Settings → Assets → Image Transforms
// Or via config:
// config/image-transforms.php (Craft 4+)
return [
'thumbnail' => ['width' => 400, 'height' => 300, 'mode' => 'crop'],
'large' => ['width' => 1200, 'height' => 630, 'mode' => 'crop'],
'avatar' => ['width' => 100, 'height' => 100, 'mode' => 'crop', 'position' => 'center-center'],
'ogImage' => ['width' => 1200, 'height' => 630, 'mode' => 'crop', 'format' => 'jpg'],
];
Development of templates for a corporate site (home, services, blog, contacts) takes 4–8 days depending on design complexity.







