Create templates

Let’s build some templates and start our Twig front end!

# Create a layout

Start by creating a layout template at templates/_layout.twig.

We want to follow DRY methodology (opens new window), which stands for Don’t Repeat Yourself. The idea is to write code once and re-use it as much as possible without duplicating things. That makes your site’s code easier to understand and maintain over time.

The layout is important for this because it will be the base from which our other templates extend.

The underscore (_) at the beginning of the filename, _layout.twig, means the template is private: unlike the quick example we started with, you cannot view the template by visiting http://tutorial.test/_layout. We can use underscores when it makes sense—in this case it’s because the layout is only a shell and isn’t meant to appear on its own.

Copy this into the templates/_layout.twig file you created:

<!DOCTYPE html>
<html lang="{{ craft.app.language }}">
  <head>
    <meta content="IE=edge" http-equiv="X-UA-Compatible">
    <meta charset="utf-8"/>
    <title>{{ siteName }}</title>
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport">
    <link href="/styles.css" rel="stylesheet">
  </head>
  <body class="ltr">
    <div class="container mx-auto px-4">
      {% block content %}
      {% endblock %}
    </div>
  </body>
</html>

This is barebones HTML that will be the foundation of every page on the site. HTML uses tags, or markup, as a structured way to organize and describe content. You may immediately recognize Craft’s siteName tag in this line:

<title>{{ siteName }}</title>

title is a standard HTML tag used to tell the browser how to label the window or tab for the page. Whatever’s wrapped in the title tag—which means whatever’s between the opening <title> and closing </title> will be displayed. You’ll recognize that {{ siteName }} we used above—this is a Twig tag Craft CMS provides for outputting our site name. This is how HTML and Twig work together in our templates.

Now download a copy of the example styles into web/styles.css: https://raw.githubusercontent.com/craftcms/tutorial-project/master/web/styles.css (opens new window)

This static stylesheet will offer some basic styling for our templates.

This base template sets the page’s language to match Craft’s and adds meta details that tell browsers how to interpret text and how the page fits into different browser windows.

A crucial part of the template is the content block:

{% block content %}
{% endblock %}

Whenever you see {{ }} or {% %} you know you’re looking at Twig tags. In this case, we’re establishing the beginning and end of a block named content. The content part could be anything we want, but here it implies that what’s inside will be page content. There’s nothing inside, however, because the layout template provides a structure that other child templates will fill in.

# Create a detail page

Now that we have a layout template, let’s use it for our blog post detail pages.

Create templates/blog/_entry.twig and add the following to it:

{% extends "_layout" %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">I’m a blog post!</h1>
{% endblock %}

The first line extends the layout template, meaning it will use that as a starting point and let us further customize or override whatever we need to. We’re now providing our own content, within the content block, to appear on the page.

Now that the blog section’s template is ready, you can visit the URL for a published post:

tutorial.test/blog/my-first-post
Screenshot of empty page with generic title

Throughout these examples, some tags have class parameters with values like text-4xl text-black font-display my-4. These are TailwindCSS utility classes (opens new window) that style elements on the page. You can ignore them or check out reference the TailwindCSS documentation (opens new window) if you want to explore customizing styles.

That’s clearly not the title we should display on every page. For any detail page template, Craft CMS provides a special entry variable we can use to access the details of the relevant entry. Let’s display the title and postDate properties in the template:




 

 


{% extends "_layout" %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time>
{% endblock %}

Now it’s looking better!

tutorial.test/blog/my-first-post
Screenshot of detail page with dynamic title and entry date

Notice how we’re using the |date Twig filter to specify formats for the entry.postDate value. This is a typical example of using a filter to modify something in Twig; a value you want to modify or transform is followed by a pipe (|), the name of the filter, and sometimes settings specific to that filter. You can see all Craft’s available filters to get a better idea of what you can do with them.

Let’s display the “Feature Image” next, using the featureImage handle we created with that custom field:








 
 
 
 
 


{% extends "_layout" %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time>

  {% if entry.featureImage|length %}
    {% for image in entry.featureImage.all() %}
      <img src="{{ image.url }}" alt="{{ image.title }}" />
    {% endfor %}
  {% endif %}
{% endblock %}

This first uses an if conditional statement (opens new window) to see whether the editor added an image in this field. The “Assets” field we used can have one or many images depending on how we configure it, so the statement uses the |length Twig filter to count the number of items—where 0 will be false and anything else will be true.

If the statement is true, meaning we have at least one feature image, we’ll use entry.featureImage.all() to get the set and a for statement to loop through and display each item using the image variable. (We limited the field settings to allow only one image, but increasing that limit means every image would be shown here!)

For each asset, we output a img HTML tag using {{ image.url }} to get the web-friendly URL for the image and {{ image.title }} as an alt tag value used for bots and screen readers.

We should now see the image after refreshing the page:

tutorial.test/blog/my-first-post
Screenshot of detail page with dynamic image added

# Transform an asset

Image Transforms let you specify the exact dimensions you need and have Craft CMS crop and size an image accordingly. Let’s specify a size for our “Feature Image”.

We’ll use Twig to create an object called featureImage with the settings we want, then pass those settings to image.getUrl() in place of image.url:



 
 
 
 
 
 








 




{% extends "_layout" %}

{% set featureImage = {
  mode: 'crop',
  width: 900,
  height: 600,
  quality: 90
} %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time>

  {% if entry.featureImage|length %}
    {% for image in entry.featureImage.all() %}
      <img src="{{ image.getUrl(featureImage) }}" alt="{{ image.title }}" />
    {% endfor %}
  {% endif %}
{% endblock %}

You can now refresh the front end and see your transformed asset:

tutorial.test/blog/my-first-post
Screenshot of detail page with auto-sized image
Automatically-resized image, cropped at 900×600px.

# Display Matrix content

Let’s output the post content stored in that Matrix field.

Matrix content is stored in whatever blocks we’ve defined. To display that content, we’ll:

  1. use another for loop to run through each block
  2. use an if statement to handle output based on the block type




















 
 
 
 
 
 
 
 
 
 
 
 
 


{% extends "_layout" %}

{% set featureImage = {
    mode: 'crop',
    width: 900,
    height: 600,
    quality: 90
} %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time>

  {% if entry.featureImage|length %}
    {% for image in entry.featureImage.all() %}
      <img src="{{ image.getUrl(featureImage) }}" alt="{{ image.title }}" />
    {% endfor %}
  {% endif %}

  <div class="my-8">
    {% for block in entry.postContent.all() %}
      <div class="my-4">
        {% if block.type == 'text' %}
          {{ block.text }}
        {% elseif block.type == 'image' %}
          {% for image in block.image.all() %}
            <img src="{{ image.url }}" alt="{{ image.title }}" />
          {% endfor %}
        {% endif %}
      </div>
    {% endfor %}
  </div>
{% endblock %}

The Matrix content will now be included on the page:

tutorial.test/blog/my-first-post
Screenshot of detail page with post content
Detail page with post content added.

# Use an include

The template’s getting longer, and we know we’ll be re-using the “Post Content” field again in the about page. Let’s tidy up a bit and make sure our Matrix code can be re-used.

To do this, we’ll move the new Twig above to its own smaller, re-usable template include.

Create templates/_includes/post-blocks.twig and copy that code to it:

<div class="my-8">
  {% for block in entry.postContent.all() %}
    <div class="my-4">
      {% if block.type == 'text' %}
        {{ block.text }}
      {% elseif block.type == 'image' %}
        {% for image in block.image.all() %}
          <img src="{{ image.url }}" alt="{{ image.title }}" />
        {% endfor %}
      {% endif %}
    </div>
  {% endfor %}
</div>

We can now go back to templates/blog/_entry.twig and embed that template instead:





















 


{% extends "_layout" %}

{% set featureImage = {
    mode: 'crop',
    width: 900,
    height: 600,
    quality: 90
} %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time>

  {% if entry.featureImage | length %}
    {% for image in entry.featureImage.all() %}
      <img src="{{ image.getUrl(featureImage) }}" alt="{{ image.title }}" />
    {% endfor %}
  {% endif %}

  {% include "_includes/post-blocks" with { blocks: entry.postContent.all() } only %}
{% endblock %}

The with { blocks: entry.postContent.all() } only part means we’re passing the entry.postContent.all() value in a variable we’ve labeled blocks. That means one small update for templates/_includes/post-blocks.twig:


 












<div class="my-8">
  {% for block in blocks %}
    <div class="my-4">
      {% if block.type == 'text' %}
        {{ block.text }}
      {% elseif block.type == 'image' %}
        {% for image in block.image.all() %}
          <img src="{{ image.url }}" alt="{{ image.title }}" />
        {% endfor %}
      {% endif %}
    </div>
  {% endfor %}
</div>

Refreshing your front end shouldn’t change a thing, but the Twig behind the scenes is now a bit more DRY and ready to re-use.

# Add code comments

While we’re building, let’s add some comments so we can remember what we’ve done. Twig ignores anything wrapped with {# and #}, which is what we’ll use to leave human-friendly notes:



 












 






 



{% extends "_layout" %}

{# create settings for image transform #}
{% set featureImage = {
    mode: 'crop',
    width: 900,
    height: 600,
    quality: 90
} %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time>

  {# output transformed feature image(s) #}
  {% if entry.featureImage | length %}
    {% for image in entry.featureImage.all() %}
      <img src="{{ image.getUrl(featureImage) }}" alt="{{ image.title }}" />
    {% endfor %}
  {% endif %}

  {# render Matrix blocks for the “Post Content” field, passed as `blocks` #}
  {% include "_includes/post-blocks" with { blocks: entry.postContent.all() } only %}
{% endblock %}

# Display post categories

The last thing we need to do on the post detail template is output the post categories using the postCategories field handle:


























 
 
 
 
 
 
 
 
 
 


{% extends "_layout" %}

{# create settings for image transform #}
{% set featureImage = {
    mode: 'crop',
    width: 900,
    height: 600,
    quality: 90
} %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <time class="text-sm block pb-4" datetime="{{ entry.postDate | date('Y-m-d') }}">{{ entry.postDate | date('d M Y') }}</time>

  {# output transformed feature image(s) #}
  {% if entry.featureImage | length %}
    {% for image in entry.featureImage.all() %}
      <img src="{{ image.getUrl(featureImage) }}" alt="{{ image.title }}" />
    {% endfor %}
  {% endif %}

  {# render Matrix blocks for the “Post Content” field, passed as `blocks` #}
  {% include "_includes/post-blocks" with { blocks: entry.postContent.all() } only %}

  {# display post categories #}
  {% if entry.postCategories|length %}
  <div class="border-t py-2 mb-6">
    {% for category in entry.postCategories.all() %}
      <a href="{{ category.url }}" class="inline-block border rounded px-2 py-1 text-sm">
        {{- category.title -}}
      </a>
    {% endfor %}
  </div>
  {% endif %}
{% endblock %}

Here we’re first checking whether there are any categories, then outputting individual links. (Those links will be broken for now, but we’ll come back to that.)

The only new thing here are the hyphens in the Twig tag around category.title: {{- and -}}. Those are used for whitespace control, so in the final page instead of...

<a href="(...)" class="inline-block border rounded px-2 py-1 text-sm">
  Category
</a>

...the empty space around the category title will be removed:

<a href="(...)" class="inline-block border rounded px-2 py-1 text-sm"
  >Category</a
>

We used a global set to store a blurb to be displayed at the bottom of all the site’s pages. Since we want that to appear everywhere, let’s add it to _layout.twig along with a copyright line:















 
 
 
 



<!DOCTYPE html>
<html lang="{{ craft.app.language }}">
  <head>
    <meta content="IE=edge" http-equiv="X-UA-Compatible">
    <meta charset="utf-8"/>
    <title>{{ siteName }}</title>
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport">
    <link href="/styles.css" rel="stylesheet">
  </head>
  <body class="ltr">
    <div class="container mx-auto px-4">
      {% block content %}
      {% endblock %}
    </div>
    <footer class="container mx-auto p-4 text-sm">
      {{ siteInformation.siteDescription|markdown }}
      <p>&copy; {{ now | date('Y') }}, built with <a class="text-blue-600" href="https://craftcms.com">Craft CMS</a></p>
    </footer>
  </body>
</html>

The “Site Description” field is Plain Text without any formatting, and just for fun we can use Craft’s markdown filter to output it in a paragaph tag (<p></p>) and support markdown syntax (opens new window).

# Add a listing page

We’ll display a listing of posts in two places: in the blog index and again on a category page where they’re limited to the selected category.

Let’s start with our landing page first.

Create templates/blog/index.twig. Any time you use index.twig or index.html, that will be the default template or page in a given folder. So when we visit http://tutorial.test/blog/, that folder’s blog/index.twig will be used for rendering the result. Add the following to that template:

{% extends "_layout" %}

{% set posts = craft.entries.section('blog').all() %}

{% block content %}

  <h1 class="text-4xl text-black font-display my-4">Blog Posts</h1>

  {% include "_includes/listing" with { posts: posts } only %}
{% endblock %}

Our entry detail page came with an automatically-available entry variable, but here we’ve fetched all the entries in the blog section and put them in a variable called posts:

{% set posts = craft.entries.section('blog').all() %}

The technical term for what we’re doing is querying entries. Once these content elements are stored in Craft CMS, there are lots of options and parameters you can use in these queries to get exactly what you need wherever you need it.

Now create templates/_includes/listing.twig. We’ll use this for listing blog entries here and re-use it again shortly:

<div class="post-list my-10 flex">
{% for post in posts %}
  <a href="{{ post.url }}" class="flex shadow-lg rounded items-center justify-center overflow-hidden">
    {% if post.featureImage|length %}
      {% set image = post.featureImage.one() %}
      <div class="w-1/4">
        <img src="{{ image.getUrl({ width: 300, height: 300}) }}"
            alt="{{ image.title }}"
            class="block"
        />
      </div>
    {% endif %}
    <span class="title w-3/4 p-4">{{ post.title }}</span>
  </a>
{% endfor %}
</div>

The image transform is similar to what we did earlier, except we used .one() instead of .all() since we only ever want just one image. This also passes an object with width and height directly to image.getUrl() instead of first assigning that object to a variable.

Here’s what the result looks like:

tutorial.test/blog
Screenshot of listing page

In this template we’ve chosen to display a square thumbnail of the “Feature Image” along with the post title. Some of these images may crop weirdly into squares, but we can use focal points to have some control over how they’re cropped!

Transformed images will automatically be cropped from the center, but a content editor may also adjust this by setting a focal point in the control panel:

  1. In the control panel, navigate to the image either using the “Assets” menu item or “Entries” and choosing the relevant blog post.
  2. Double-click the asset, then choose “Edit” from the top-right corner of the image preview. (This will open the editor.)
  3. Choose the “Focal Point” tool, dragging the focal point bullseye icon to an important area of the image.
  4. Choose “Save”.

Setting a focal point on an asset

Back on the front end, refresh the listing page and you’ll see the re-cropped thumbnail:

tutorial.test/blog
Screenshot of listing page where post thumbnail is cropped toward focal point of image
Listing page with adjusted thumbnail focal point.

# Create a category listing

Create templates/blog/_category.twig and add the following:

{% extends "_layout" %}

{% set posts = craft.entries.section('blog').relatedTo(category).all() %}

{% block content %}

  <h1 class="text-4xl text-black font-display my-4">
    Blog Posts in “{{ category.title }}</h1>

  {% include "_includes/listing" with { posts: posts } only %}
{% endblock %}

In the same way that entry detail pages came automatically loaded with an entry variable, category pages come with a special category variable. We’re using that here to limit only to posts in the selected category using the relatedTo query parameter:

{% set posts = craft.entries.section('blog').relatedTo(category).all() %}

Our post category listings, which you can navigate to by choosing any of a blog post’s tags, should be working now:

tutorial.test/blog/category/ramblings
Screenshot of listing page limited by category
Listing page for posts in the `Ramblings` category.

# Add navigation

Getting around is pretty awkward right now. Let’s add some navigation.

Create templates/_includes/nav.twig and add the following to it:

<nav class="container mx-auto py-4 px-4" role="navigation" aria-label="Main">
  {% set firstSegment = craft.app.request.getSegment(1) %}
  <ul class="flex">
    <li class="mr-6">
      <a class="text-blue-600 {{ firstSegment == '' ? 'border-b border-blue-400' }}" href="{{ siteUrl }}">Home</a>
    </li>
    <li class="mr-6">
      <a class="text-blue-600 {{ firstSegment == 'blog' ? 'border-b border-blue-400' }}" href="{{ url('blog') }}">Blog</a>
    </li>
    <li class="mr-6">
      <a class="text-blue-600 {{ firstSegment == 'about' ? 'border-b border-blue-400' }}" href="{{ url('about') }}">About</a>
    </li>
  </ul>
</nav>

Now let’s include that in templates/_layout.twig:











 











<!DOCTYPE html>
<html lang="{{ craft.app.language }}">
  <head>
    <meta content="IE=edge" http-equiv="X-UA-Compatible">
    <meta charset="utf-8"/>
    <title>{{ siteName }}</title>
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport">
    <link href="/styles.css" rel="stylesheet">
  </head>
  <body class="ltr">
    {% include "_includes/nav" %}
    <div class="container mx-auto px-4">
      {% block content %}
      {% endblock %}
    </div>
    <footer class="container mx-auto mt-8 p-4 text-sm opacity-50">
      {{ siteInformation.siteDescription|markdown }}
      <p>&copy; {{ now | date('Y') }}, built with <a class="text-blue-600" href="https://craftcms.com">Craft CMS</a></p>
    </footer>
  </body>
</html>
tutorial.test/blog
Screenshot of blog listing with new top navigation
Blog listing with navigation.

# Add a template for a single

Next, let’s set up the about page. We set it up as a single, using the template _singles/about.

Create template/_singles/about.twig and add the following to it:

{% extends "_layout" %}

{% block content %}
  <h1 class="text-4xl text-black font-display my-4">{{ entry.title }}</h1>

  <div class="flex -mx-4">
    <div class="w-1/2 px-4">
      {% if entry.aboutImage | length %}
        {% for image in entry.aboutImage.all() %}
          <img src="{{ image.getUrl({ width: 800, height: 600 }) }}" alt="{{ image.title }}" class="my-8" />
        {% endfor %}
      {% endif %}
    </div>
    <div class="w-1/2 px-4">
      {% include "_includes/post-blocks" with { blocks: entry.postContent.all() } only %}
    </div>
  </div>
{% endblock %}

We’re using some CSS utility classes to create a two-column layout here, but otherwise you’ll recognize all the pieces from previous examples!

tutorial.test/about
Screenshot of two-colum About page
The About page.