Content Pages and 404 Intermediate 12 min read

Blog and Article Templates

Create engaging blog experiences in Shopify with customizable blog and article templates, including listings, individual posts, and dynamic content.

Shopify’s blog system provides a platform for content marketing, SEO, and customer engagement. Understanding blog and article templates lets you create polished content experiences.

Blog vs Article Objects

Shopify separates blogs (containers) from articles (individual posts):

{# Blog object (on blog pages) #}
{{ blog.title }}
{{ blog.handle }}
{{ blog.url }}
{{ blog.articles_count }}
{{ blog.articles }}
{{ blog.all_tags }}
{# Article object (on article pages) #}
{{ article.title }}
{{ article.author }}
{{ article.content }}
{{ article.published_at }}
{{ article.tags }}
{{ article.excerpt_or_content }}
{{ article.image }}
{{ article.comments }}

Blog Listing Template

{# templates/blog.json #}
{
"sections": {
"main": {
"type": "blog-posts",
"settings": {
"posts_per_page": 12,
"show_featured_image": true,
"show_date": true,
"show_author": true,
"show_tags": true
}
},
"newsletter": {
"type": "newsletter",
"settings": {}
}
},
"order": ["main", "newsletter"]
}

Blog Posts Section

{# sections/blog-posts.liquid #}
{%- paginate blog.articles by section.settings.posts_per_page -%}
<section class="blog-posts section-{{ section.id }}">
<div class="container">
<header class="blog-posts__header">
<h1 class="blog-posts__title">{{ blog.title }}</h1>
{%- if blog.all_tags.size > 0 -%}
<nav class="blog-posts__tags" aria-label="Blog categories">
<a
href="{{ blog.url }}"
class="blog-posts__tag{% unless current_tags %} blog-posts__tag--active{% endunless %}"
>
All
</a>
{%- for tag in blog.all_tags -%}
<a
href="{{ blog.url }}/tagged/{{ tag | handle }}"
class="blog-posts__tag{% if current_tags contains tag %} blog-posts__tag--active{% endif %}"
>
{{ tag }}
</a>
{%- endfor -%}
</nav>
{%- endif -%}
</header>
{%- if blog.articles_count > 0 -%}
<div class="blog-posts__grid">
{%- for article in blog.articles -%}
<article class="article-card">
{%- if section.settings.show_featured_image and article.image -%}
<a href="{{ article.url }}" class="article-card__image-link">
{{ article.image | image_url: width: 800 | image_tag:
loading: 'lazy',
class: 'article-card__image',
sizes: '(max-width: 768px) 100vw, 400px'
}}
</a>
{%- endif -%}
<div class="article-card__content">
{%- if section.settings.show_tags and article.tags.size > 0 -%}
<div class="article-card__tags">
{%- for tag in article.tags limit: 2 -%}
<span class="article-card__tag">{{ tag }}</span>
{%- endfor -%}
</div>
{%- endif -%}
<h2 class="article-card__title">
<a href="{{ article.url }}">{{ article.title }}</a>
</h2>
{%- if section.settings.show_excerpt -%}
<p class="article-card__excerpt">
{{ article.excerpt_or_content | strip_html | truncate: 150 }}
</p>
{%- endif -%}
<footer class="article-card__meta">
{%- if section.settings.show_author -%}
<span class="article-card__author">{{ article.author }}</span>
{%- endif -%}
{%- if section.settings.show_date -%}
<time
class="article-card__date"
datetime="{{ article.published_at | date: '%Y-%m-%d' }}"
>
{{ article.published_at | date: format: 'abbreviated_date' }}
</time>
{%- endif -%}
</footer>
</div>
</article>
{%- endfor -%}
</div>
{%- if paginate.pages > 1 -%}
<nav class="blog-posts__pagination" aria-label="Blog pagination">
{% render 'pagination', paginate: paginate %}
</nav>
{%- endif -%}
{%- else -%}
<p class="blog-posts__empty">No articles found.</p>
{%- endif -%}
</div>
</section>
{%- endpaginate -%}
{% schema %}
{
"name": "Blog Posts",
"settings": [
{
"type": "range",
"id": "posts_per_page",
"label": "Posts per page",
"min": 6,
"max": 24,
"step": 3,
"default": 12
},
{
"type": "checkbox",
"id": "show_featured_image",
"label": "Show featured image",
"default": true
},
{
"type": "checkbox",
"id": "show_excerpt",
"label": "Show excerpt",
"default": true
},
{
"type": "checkbox",
"id": "show_date",
"label": "Show date",
"default": true
},
{
"type": "checkbox",
"id": "show_author",
"label": "Show author",
"default": true
},
{
"type": "checkbox",
"id": "show_tags",
"label": "Show tags",
"default": true
}
]
}
{% endschema %}

Article Template

{# templates/article.json #}
{
"sections": {
"main": {
"type": "article-content",
"settings": {
"show_featured_image": true,
"show_author": true,
"show_date": true,
"show_share": true
}
},
"comments": {
"type": "article-comments",
"settings": {}
},
"related": {
"type": "related-articles",
"settings": {
"heading": "Related Articles"
}
}
},
"order": ["main", "comments", "related"]
}

Article Content Section

{# sections/article-content.liquid #}
<article class="article section-{{ section.id }}" itemscope itemtype="http://schema.org/BlogPosting">
<div class="container container--narrow">
<header class="article__header">
{%- if article.tags.size > 0 -%}
<div class="article__tags">
{%- for tag in article.tags -%}
<a href="{{ blog.url }}/tagged/{{ tag | handle }}" class="article__tag">
{{ tag }}
</a>
{%- endfor -%}
</div>
{%- endif -%}
<h1 class="article__title" itemprop="headline">{{ article.title }}</h1>
<div class="article__meta">
{%- if section.settings.show_author -%}
<span class="article__author" itemprop="author">{{ article.author }}</span>
{%- endif -%}
{%- if section.settings.show_date -%}
<time
class="article__date"
datetime="{{ article.published_at | date: '%Y-%m-%d' }}"
itemprop="datePublished"
>
{{ article.published_at | date: format: 'month_day_year' }}
</time>
{%- endif -%}
<span class="article__reading-time">
{%- assign words = article.content | strip_html | split: ' ' | size -%}
{%- assign minutes = words | divided_by: 200 -%}
{%- if minutes < 1 %}{% assign minutes = 1 %}{% endif -%}
{{ minutes }} min read
</span>
</div>
</header>
{%- if section.settings.show_featured_image and article.image -%}
<figure class="article__featured-image">
{{ article.image | image_url: width: 1200 | image_tag:
loading: 'eager',
class: 'article__image',
itemprop: 'image'
}}
</figure>
{%- endif -%}
<div class="article__content rte" itemprop="articleBody">
{{ article.content }}
</div>
<footer class="article__footer">
{%- if section.settings.show_share -%}
<div class="article__share">
<span class="article__share-label">Share:</span>
<a
href="https://twitter.com/intent/tweet?url={{ shop.url }}{{ article.url }}&text={{ article.title | url_encode }}"
target="_blank"
rel="noopener"
class="article__share-link"
aria-label="Share on Twitter"
>
Twitter
</a>
<a
href="https://www.facebook.com/sharer/sharer.php?u={{ shop.url }}{{ article.url }}"
target="_blank"
rel="noopener"
class="article__share-link"
aria-label="Share on Facebook"
>
Facebook
</a>
<a
href="https://www.linkedin.com/sharing/share-offsite/?url={{ shop.url }}{{ article.url }}"
target="_blank"
rel="noopener"
class="article__share-link"
aria-label="Share on LinkedIn"
>
LinkedIn
</a>
</div>
{%- endif -%}
<nav class="article__nav">
{%- if blog.previous_article -%}
<a href="{{ blog.previous_article }}" class="article__nav-link article__nav-link--prev">
← Previous Article
</a>
{%- endif -%}
{%- if blog.next_article -%}
<a href="{{ blog.next_article }}" class="article__nav-link article__nav-link--next">
Next Article →
</a>
{%- endif -%}
</nav>
</footer>
</div>
</article>
{% schema %}
{
"name": "Article Content",
"settings": [
{
"type": "checkbox",
"id": "show_featured_image",
"label": "Show featured image",
"default": true
},
{
"type": "checkbox",
"id": "show_author",
"label": "Show author",
"default": true
},
{
"type": "checkbox",
"id": "show_date",
"label": "Show date",
"default": true
},
{
"type": "checkbox",
"id": "show_share",
"label": "Show share links",
"default": true
}
]
}
{% endschema %}

Article Comments Section

{# sections/article-comments.liquid #}
{%- if blog.comments_enabled? -%}
<section class="article-comments section-{{ section.id }}">
<div class="container container--narrow">
<h2 class="article-comments__heading">
Comments ({{ article.comments_count }})
</h2>
{# Comment form #}
{%- form 'new_comment', article, class: 'comment-form' -%}
{%- if form.posted_successfully? -%}
{%- if blog.moderated? -%}
<div class="comment-form__success" role="status">
Your comment has been submitted and is awaiting moderation.
</div>
{%- else -%}
<div class="comment-form__success" role="status">
Your comment has been posted!
</div>
{%- endif -%}
{%- endif -%}
{%- if form.errors -%}
<div class="comment-form__errors" role="alert">
{{ form.errors | default_errors }}
</div>
{%- endif -%}
<div class="comment-form__fields">
<div class="comment-form__field">
<label for="comment-author">Name</label>
<input
type="text"
id="comment-author"
name="comment[author]"
value="{{ form.author }}"
required
>
</div>
<div class="comment-form__field">
<label for="comment-email">Email</label>
<input
type="email"
id="comment-email"
name="comment[email]"
value="{{ form.email }}"
required
>
</div>
<div class="comment-form__field comment-form__field--full">
<label for="comment-body">Comment</label>
<textarea
id="comment-body"
name="comment[body]"
rows="5"
required
>{{ form.body }}</textarea>
</div>
</div>
<button type="submit" class="button button--primary">
Post Comment
</button>
{%- endform -%}
{# Comment list #}
{%- if article.comments_count > 0 -%}
{%- paginate article.comments by 20 -%}
<div class="comments-list">
{%- for comment in article.comments -%}
<div class="comment" id="comment-{{ comment.id }}">
<div class="comment__avatar">
{%- assign initials = comment.author | slice: 0 | upcase -%}
<span class="comment__initials">{{ initials }}</span>
</div>
<div class="comment__content">
<header class="comment__header">
<span class="comment__author">{{ comment.author }}</span>
<time
class="comment__date"
datetime="{{ comment.created_at | date: '%Y-%m-%d' }}"
>
{{ comment.created_at | date: format: 'abbreviated_date' }}
</time>
</header>
<div class="comment__body">
{{ comment.content }}
</div>
</div>
</div>
{%- endfor -%}
</div>
{%- if paginate.pages > 1 -%}
{% render 'pagination', paginate: paginate %}
{%- endif -%}
{%- endpaginate -%}
{%- endif -%}
</div>
</section>
{%- endif -%}
{% schema %}
{
"name": "Article Comments",
"settings": []
}
{% endschema %}
{# sections/related-articles.liquid #}
{%- assign related_articles = blog.articles | where_not: 'id', article.id -%}
{%- if article.tags.size > 0 -%}
{%- assign tag_articles = blank | split: '' -%}
{%- for tag in article.tags -%}
{%- for art in blog.articles -%}
{%- if art.tags contains tag and art.id != article.id -%}
{%- unless tag_articles contains art -%}
{%- assign tag_articles = tag_articles | push: art -%}
{%- endunless -%}
{%- endif -%}
{%- endfor -%}
{%- endfor -%}
{%- if tag_articles.size > 0 -%}
{%- assign related_articles = tag_articles -%}
{%- endif -%}
{%- endif -%}
{%- if related_articles.size > 0 -%}
<section class="related-articles section-{{ section.id }}">
<div class="container">
<h2 class="related-articles__heading">{{ section.settings.heading }}</h2>
<div class="related-articles__grid">
{%- for art in related_articles limit: section.settings.articles_count -%}
<article class="article-card article-card--small">
{%- if art.image -%}
<a href="{{ art.url }}" class="article-card__image-link">
{{ art.image | image_url: width: 400 | image_tag:
loading: 'lazy',
class: 'article-card__image'
}}
</a>
{%- endif -%}
<div class="article-card__content">
<h3 class="article-card__title">
<a href="{{ art.url }}">{{ art.title }}</a>
</h3>
<time class="article-card__date">
{{ art.published_at | date: format: 'abbreviated_date' }}
</time>
</div>
</article>
{%- endfor -%}
</div>
</div>
</section>
{%- endif -%}
{% schema %}
{
"name": "Related Articles",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Related Articles"
},
{
"type": "range",
"id": "articles_count",
"label": "Number of articles",
"min": 2,
"max": 6,
"default": 3
}
]
}
{% endschema %}

Blog Styles

/* Blog posts grid */
.blog-posts__header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.blog-posts__title {
font-size: clamp(2rem, 5vw, 3rem);
margin-bottom: var(--spacing-md);
}
.blog-posts__tags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
justify-content: center;
}
.blog-posts__tag {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--color-background-subtle);
border-radius: var(--border-radius);
text-decoration: none;
font-size: 0.875rem;
}
.blog-posts__tag--active {
background: var(--color-primary);
color: white;
}
.blog-posts__grid {
display: grid;
gap: var(--spacing-xl);
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
/* Article card */
.article-card {
display: flex;
flex-direction: column;
}
.article-card__image-link {
display: block;
overflow: hidden;
border-radius: var(--border-radius);
margin-bottom: var(--spacing-md);
}
.article-card__image {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
transition: transform 0.3s ease;
}
.article-card:hover .article-card__image {
transform: scale(1.05);
}
.article-card__tags {
display: flex;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
}
.article-card__tag {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-primary);
}
.article-card__title {
font-size: 1.25rem;
margin-bottom: var(--spacing-sm);
}
.article-card__title a {
text-decoration: none;
color: inherit;
}
.article-card__title a:hover {
color: var(--color-primary);
}
.article-card__excerpt {
color: var(--color-text-light);
line-height: 1.6;
margin-bottom: var(--spacing-sm);
}
.article-card__meta {
display: flex;
gap: var(--spacing-md);
font-size: 0.875rem;
color: var(--color-text-light);
margin-top: auto;
}
/* Article page */
.article__header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.article__tags {
display: flex;
gap: var(--spacing-sm);
justify-content: center;
margin-bottom: var(--spacing-md);
}
.article__tag {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-primary);
text-decoration: none;
}
.article__title {
font-size: clamp(2rem, 5vw, 3rem);
line-height: 1.2;
margin-bottom: var(--spacing-md);
}
.article__meta {
display: flex;
gap: var(--spacing-md);
justify-content: center;
font-size: 0.875rem;
color: var(--color-text-light);
}
.article__featured-image {
margin: 0 0 var(--spacing-xl);
}
.article__image {
width: 100%;
border-radius: var(--border-radius-lg);
}
.article__content {
font-size: 1.125rem;
line-height: 1.8;
}
.article__footer {
margin-top: var(--spacing-2xl);
padding-top: var(--spacing-xl);
border-top: 1px solid var(--color-border);
}
.article__share {
display: flex;
gap: var(--spacing-md);
align-items: center;
margin-bottom: var(--spacing-lg);
}
.article__nav {
display: flex;
justify-content: space-between;
}
/* Comments */
.comment-form__fields {
display: grid;
gap: var(--spacing-md);
grid-template-columns: 1fr 1fr;
margin-bottom: var(--spacing-md);
}
.comment-form__field--full {
grid-column: span 2;
}
.comments-list {
margin-top: var(--spacing-xl);
}
.comment {
display: flex;
gap: var(--spacing-md);
padding: var(--spacing-lg) 0;
border-bottom: 1px solid var(--color-border);
}
.comment__avatar {
flex-shrink: 0;
}
.comment__initials {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: var(--color-primary);
color: white;
border-radius: 50%;
font-weight: 600;
}
.comment__author {
font-weight: 600;
margin-right: var(--spacing-sm);
}
.comment__date {
font-size: 0.875rem;
color: var(--color-text-light);
}
/* Related articles */
.related-articles__grid {
display: grid;
gap: var(--spacing-lg);
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}

SEO Considerations

Add structured data for articles:

{# In article template or section #}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": {{ article.title | json }},
"image": {{ article.image | image_url: width: 1200 | json }},
"datePublished": {{ article.published_at | date: '%Y-%m-%dT%H:%M:%S%z' | json }},
"dateModified": {{ article.updated_at | date: '%Y-%m-%dT%H:%M:%S%z' | json }},
"author": {
"@type": "Person",
"name": {{ article.author | json }}
},
"publisher": {
"@type": "Organization",
"name": {{ shop.name | json }},
"logo": {
"@type": "ImageObject",
"url": {{ shop.url | append: settings.logo | image_url: width: 300 | json }}
}
}
}
</script>

Practice Exercise

Create blog templates for:

  1. A standard blog listing with filtering by tag
  2. An article page with share buttons and author bio
  3. A related articles section using tags

Test with:

  • Multiple blog posts
  • Various tag combinations
  • Comment functionality

Key Takeaways

  1. blog object contains articles and tags
  2. article object has content, author, date, tags
  3. Pagination with {% paginate blog.articles %}
  4. Tag filtering via URL (/tagged/tagname)
  5. Comments with {% form 'new_comment', article %}
  6. Reading time calculated from word count
  7. Schema.org markup for SEO benefits

What’s Next?

The next lesson covers 404 Page Design and Navigation Recovery for creating helpful error pages.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...