Skip to content

How To: Create a Blog

This guide walks you through building a blog with Alchemy. Each blog post is a page with its own URL, giving you a built-in publishing workflow, publication dates, and tagging support. The blog overview page automatically lists all published posts.

You will create two page layouts — one for the overview and one for individual posts — along with their elements and view partials.

Define Elements

Define your blog elements in config/alchemy/elements.yml.

Post Header

The post header captures all metadata in a single element — headline, author, hero image, and a teaser excerpt:

yaml
# config/alchemy/elements.yml
- name: post_header
  unique: true
  taggable: true
  ingredients:
    - role: headline
      type: Headline
      settings:
        levels: [1]
    - role: author
      type: Text
    - role: image
      type: Picture
      settings:
        size: 1200x600
        crop: true
    - role: teaser
      type: Richtext

TIP

The post's title, URL, and publication date come from the page. The teaser is used as a short excerpt on the overview page. With taggable: true, editors can tag posts with categories.

Post Text

A repeatable text section with a headline. The anchor: from_value setting auto-generates an id attribute from the headline text, enabling deep links to sections within a post:

yaml
- name: post_text
  ingredients:
    - role: headline
      type: Headline
      settings:
        levels: [2, 3]
        anchor: from_value
    - role: text
      type: Richtext

Post Picture

A repeatable image element with responsive srcset support:

yaml
- name: post_picture
  ingredients:
    - role: image
      type: Picture
      settings:
        srcset: ["400x", "800x", "1200x"]
        sizes: ["(max-width: 600px) 400px", "(max-width: 900px) 800px", "1200px"]

Blog Overview

The overview element with an editable headline. Its partial queries and renders the blog posts automatically:

yaml
- name: blog_overview
  unique: true
  ingredients:
    - role: headline
      type: Headline
      settings:
        levels: [1]
      default: Blog

Define Page Layouts

yaml
# config/alchemy/page_layouts.yml
- name: blog
  unique: true
  cache: false # overview lists child pages, must reflect publish changes
  elements:
    - blog_overview
  autogenerate:
    - blog_overview

- name: blogpost
  elements:
    - post_header
    - post_text
    - post_picture
  autogenerate:
    - post_header

TIP

Editors create new blog posts by adding child pages under the blog overview page and selecting the blogpost layout.

Customize Views

Generate the view partials for your elements and page layouts:

bash
bin/rails g alchemy:elements --skip
bin/rails g alchemy:page_layouts --skip

The generated page layout partials render all elements with <%= render_elements %>. For the blog post, wrap it in an <article> tag in app/views/alchemy/page_layouts/_blogpost.html.erb:

erb
<article class="blog-post">
  <%= render_elements %>
</article>

Then customize the element partials in app/views/alchemy/elements/:

_post_header.html.erb

erb
<%= element_view_for(post_header, tag: "header") do |el| %>
  <%= el.render :headline %>
  <p class="meta">
    By <%= el.render :author %> &middot;
    <time><%= l(page.public_on, format: :long) %></time>
  </p>
  <%= el.render :teaser %>
  <%= el.render :image %>
<% end %>

_post_text.html.erb

erb
<%= element_view_for(post_text) do |el| %>
  <%= el.render :headline %>
  <%= el.render :text %>
<% end %>

_post_picture.html.erb

erb
<%= element_view_for(post_picture) do |el| %>
  <%= el.render :image %>
<% end %>

_blog_overview.html.erb

The overview element lists all published child pages:

erb
<%= element_view_for(blog_overview) do |el| %>
  <%= el.render :headline %>
  <% posts = blog_overview.page.children.published.order(public_on: :desc) %>
  <%= render partial: "post_preview", collection: posts, as: :post %>
<% end %>

_post_preview.html.erb

erb
<% cache(post) do %>
  <% header = post.elements.find_by(name: "post_header") %>
  <% return unless header %>
  <article class="post-preview">
    <% if header.ingredient_by_role(:image)&.picture %>
      <%= render header.ingredient_by_role(:image), size: "600x300", crop: true %>
    <% end %>
    <h2><%= link_to post.title, show_alchemy_page_path(post) %></h2>
    <time><%= l(post.public_on, format: :long) %></time>
    <p><%= header.ingredient_by_role(:teaser)&.value&.truncate(200) %></p>
  </article>
<% end %>

BSD-3 Licensed · Hosting sponsored by netlify