Skip to content

Elements

Elements are the central building blocks of an Alchemy website. They are reusable content containers - for example, a headline, an image and a text block might form an "article" element. These individual pieces of content within an element are called ingredients.

You define elements and their ingredients in YAML. Editors place them on pages through the admin interface. You control how they are rendered with view partials. See the About guide for how elements fit into the full content architecture.

Defining elements

Elements are defined in the config/alchemy/elements.yml file. Element definitions are written in YAML.

If this file does not exist yet, create it with the scaffold generator:

bash
bin/rails g alchemy:install

The generator also creates the other basic folders and files for setting up your website with Alchemy.

Example Element Definition

yaml
# config/alchemy/elements.yml
- name: article
  unique: true
  ingredients:
    - role: image
      type: Picture
    - role: headline
      type: Text
      as_element_title: true
    - role: copy
      type: Richtext

The element in this example is named "article" and can be placed only once per page (because it is unique). It has three ingredients of the following types:

TIP

By default, the first ingredient's value is used as the preview text in the element's title bar in the admin frontend. If you want a different ingredient to be used instead, add as_element_title: true to that ingredient. In the example above, the "headline" ingredient would be used instead of "image".

Element Settings

The following settings can be used to define elements in the elements.yml.

name

String required

A lowercased unique name of the element. Separate words need to be underscored. The name is used in the page_layouts.yml file to define on which pages the element can be used. It is also the file name of the element's view partial in app/views/alchemy/elements/. The name is translatable for the admin interface.

unique

Boolean (Default: false)

Passing true means this element can be placed only once on a page. For more fine-grained control, use amount instead.

hint

String

A hint for the user in the admin interface that describes what the element is used for. Set to true to use a translated hint from your locale files. The hint is displayed as a small question mark icon that reveals a tooltip on hover.

message

String

A prominent informational message displayed at the top of the element editor form in the admin interface. You can use simple HTML to add emphasis.

warning

String

A prominent warning message displayed at the top of the element editor form in the admin interface. You can use simple HTML to add emphasis.

nestable_elements

Array

A collection of element names that can be nested into the element.

taggable

Boolean (Default: false)

Enables the element to be taggable by the user in the admin frontend.

fixed

Boolean (Default: false)

Separates an element from the normal flow. When true, the element is only rendered when explicitly requested via the fixed_elements scope. See fixed elements for more details.

icon

String|Boolean

Controls the icon in the admin UI. Set to true to use <element_name>.svg or set to a string to use <string>.svg, both from app/assets/images/alchemy/element_icons/ in your app. If not set, Alchemy uses its own default icon.

TIP

Alchemy uses Remix Icons throughout its admin interface. To keep your custom element icons consistent, download SVG icons from the Remix Icon website and place them in the element_icons folder.

amount

Integer

Maximum number of top-level instances of this element per page. Once the limit is reached, the element is no longer offered in the "add element" dialog. All elements on the draft version are counted regardless of their published state. Defaults to unlimited. Does not apply to nested elements. For a single instance, use unique instead.

yaml
- name: hero_banner
  amount: 3

compact

Boolean (Default: false)

Renders the element in a compact UI in the admin editor. Useful for elements that are primarily used as nestable children, like slides in a slider, cards in a card grid, or items in a gallery.

yaml
- name: slide
  compact: true
  ingredients:
    - role: image
      type: Picture

searchable

Boolean (Default: true)

Include this element's ingredients in the fulltext search index. Set to false for elements that contain sensitive data or purely controlling values like CSS class names, accordion titles, or slider timing configurations.

deprecated

Boolean|String (Default: false)

Mark this element as deprecated. Set to true to use a translated deprecation notice, or provide a custom message string directly.

yaml
- name: old_element
  deprecated: true

- name: legacy_widget
  deprecated: "Use the new_widget element instead."

When set to true, Alchemy looks up the notice via I18n:

yaml
# config/locales/en.yml
en:
  alchemy:
    element_deprecation_notices:
      old_element: "This element is outdated. Please use new_element instead."

ingredients

Array

A collection of ingredients the element contains. An ingredient has to have a role (unique per element) and a type.

TIP

Have a look at the ingredients guide to get more information about available ingredient types.

Available Elements

Before you can use elements on pages, you need to define which page layouts they can be placed on.

Open config/alchemy/page_layouts.yml and add the element name to the list of available elements for a page layout.

yaml
- name: standard
  elements: [article]
  autogenerate: [article]

You can now place the article element on any page with the standard page layout.

Any new pages created with the standard layout will automatically have the article element.

Nestable Elements

Elements can be nested inside other elements. Define the allowed children in your elements.yml file.

Example

yaml
- name: article
  ingredients:
    - role: headline
      type: Text
  nestable_elements:
    - text
    - picture

- name: text
  ingredients:
    - role: text
      type: Richtext

- name: picture
  ingredients:
    - role: text
      type: Picture

TIP

Nested elements can also have nestable_elements. Just don't get too crazy with it, though.

Rendering Nested Elements

Use the render helper to render all nested elements as a collection.

erb
<%= element_view_for(article) do |el| %>
  <h3><%= el.render :headline %></h3>

  <div class="text-blocks">
    <%= render article.nested_elements %>
  </div>
<% end %>

You can also filter nested elements by name.

erb
<%= element_view_for(article) do |el| %>
  <h3><%= el.render :headline %></h3>

  <div class="text-blocks">
    <%= render article.nested_elements.named(:text) %>
  </div>

  <div class="pictures">
    <%= render article.nested_elements.named(:picture) %>
  </div>
<% end %>

Or render a single nested element.

erb
<%= element_view_for(article) do |el| %>
  <h3><%= el.render :headline %></h3>

  <div class="picture">
    <%= render article.nested_elements.find_by(name: :picture) %>
  </div>

  <div class="text-blocks">
    <%= render article.nested_elements.where(name: :text) %>
  </div>
<% end %>

Ingredient Groups

Ingredients can be visually grouped in the admin editor using the group property on ingredient definitions.

yaml
- name: product
  ingredients:
    - role: title
      type: Text
    - role: description
      type: Richtext
    - role: css_class
      type: Select
      group: settings
    - role: width
      type: Text
      group: settings

Grouped ingredients are rendered as collapsible sections in the element editor. Ingredients without a group appear ungrouped at the top.

TIP

Use ingredient groups sparingly. Editors should see all content-related ingredients at once without having to expand sections. Groups are best suited for configuration or secondary options that are not part of the main content, such as CSS classes, display settings, or layout options.

Group names can be translated via I18n.

yaml
# config/locales/en.yml
en:
  alchemy:
    element_groups:
      settings: Settings
      product:
        settings: Display Options

Tagging

Elements are taggable. To enable it, add taggable: true to the element's definition.

yaml
- name: article
  taggable: true
  ingredients:
    - role: image
      type: Picture
    - role: headline
      type: Text
      as_element_title: true
    - role: copy
      type: Richtext

Tags are a collection on the element object. element.tag_list returns an array of tag names.

erb
<%= element.tag_list.join(', ') %>

Alchemy uses the gutentag gem, so please refer to the github README or the Wiki for further information.

Publishing

Only published elements are rendered on the public page version. Editors can toggle element visibility in the admin.

Time-Based Publishing 8.1+

Beginning with Alchemy 8.1, elements support time-based publishing, just like pages. Each element has public_on and public_until timestamps that control when it is visible to visitors.

  • public_on — the date and time the element becomes visible (defaults to the current time when created)
  • public_until — the date and time the element is automatically hidden

TIP

This is useful for time-limited content like banners, seasonal promotions, or event announcements that should appear and disappear automatically.

Validations

You can enable validations for your ingredients. They behave like the Rails model validations.

Supported validations are:

  • presence
  • uniqueness
  • format

The format validator needs to have a regular expression or a predefined matcher string as its value.

Predefined format matchers are configured in your initializer. You can also add your own. See the Configuration guide for details.

Format Matchers

ruby
# config/initializers/alchemy.rb
Alchemy.configure do |config|
  config.format_matchers.tap do |format|
    format.email = /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
    format.url = /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\z/ix
  end
end

Example

yaml
- name: person
  ingredients:
    - role: name
      type: Text
      validate:
        - presence: true
    - role: email
      type: Text
      validate:
        - format: email
    - role: homepage
      type: Text
      validate:
        - format: !ruby/regexp '/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/']

The email ingredient gets validated against the predefined email matcher from the initializer.

The homepage ingredient is matched against the given regexp.

Multiple Validations

Ingredients can have multiple validations.

yaml
- name: person
  ingredients:
    - role: name
      type: Text
      validate:
        - presence: true
        - uniqueness: true
        - format: name

Validations are evaluated in the order they are defined.

The name ingredient will be validated for presence first, then for uniqueness, and finally against its format.

Rendering

Generating Partials

Each element has a view partial (a Rails partial) that lives in app/views/alchemy/elements/.

You don't need to create these files yourself. Use the Rails generator to create them.

bash
bin/rails g alchemy:elements --skip

The --skip flag skips files that already exist. The --force flag will overwrite your element view partials automatically.

TIP

You can pass --template-engine or -e as an argument to use haml, slim or erb. The default template engine depends on your settings in your Rails host app.

The generator will create partials for each element in your app/views/alchemy/elements folder.

For the article element from the example above, the generator creates _article.html.erb. The generated code serves as a starting point that you can customize to fit your needs.

Page Layouts

With the article element associated with the 'standard' page layout, it can be rendered in the layout partial app/views/alchemy/page_layouts/_standard.html.erb.

erb
...
<div class="row">
  <%= render_elements %>
</div>
...

This renders all elements from the current page.

TIP

Pages must be published for elements to be rendered.

If you aren't seeing content you created in the admin interface, make sure elements are saved and you click the "Publish current page content" button from the edit page admin view.

Render Only Specific Elements

Sometimes you only want to render specific elements on a specific page and maybe exclude some elements.

erb
<body>
  <header><%= render_elements only: 'header' %></header>

  <main>
    <%= render_elements except: %w(header footer) %>
  </main>

  <footer><%= render_elements only: 'footer' %></footer>
</body>

render_elements Options

The render_elements helper accepts several options:

  • only - Render only elements with these names
  • except - Render all elements except these names
  • from_page - Render elements from a different page. Accepts a page layout name as a String, an Array of page layout names, or an Alchemy::Page instance
  • count - Limit the number of rendered elements
  • offset - Skip the first N elements
  • random - Randomize the order of elements
  • fixed - When true, render only fixed elements. See rendering fixed elements

Render Elements from Other Pages

A common use case is to have global pages for header and footer parts.

yaml
# config/alchemy/elements.yml
- name: header
  hint: Navigation bar at the top of every page
  ingredients:
    # ...

- name: footer
  hint: Footer section at the bottom of every page
  ingredients:
    # ...

# config/alchemy/page_layouts.yml

- name: header
  unique: true
  elements: [header]
  autogenerate: [header]
  layoutpage: true

- name: footer
  unique: true
  elements: [footer]
  autogenerate: [footer]
  layoutpage: true

These can be rendered in your application.html.erb file.

erb
<!DOCTYPE html>
<html lang="<%= @page.language_code %>">
  <head>
    ...
  </head>

  <body>
    <header><%= render_elements from_page: 'header' %></header>

    <main>
      <%= yield %>
    </main>

    <footer><%= render_elements from_page: 'footer' %></footer>

    <%= render "alchemy/edit_mode" %>
  </body>
</html>

Rendering Fixed Elements

Often you have a separate section on one page (like a sidebar) or a global section to be rendered on every page (like a navbar or footer).

If you configure those elements as fixed: true in elements.yml, then they'll be separated from the general collection of elements and will be displayed in a separate tab in the admin elements section.

yaml
- name: sidebar
  unique: true
  fixed: true
  ingredients:
    - role: name
      type: Text
      # ...

You can render fixed elements using render_elements with the fixed option.

erb
<aside>
  <%= render_elements fixed: true, only: 'sidebar' %>
</aside>

You can also access them directly via the page.

erb
<%= render @page.fixed_elements.named('sidebar') %>

Element Views

The Alchemy element generator creates a basic view partial for each element. This is where you control how the element's content is rendered on your website.

erb
<%= element_view_for(element) do |el| %>
  <h3><%= el.render :headline %></h3>
  <div class="row">
    <div class="large-6 columns">
      <p>
        <%= el.render :image %>
      </p>
    </div>
    <div class="large-6 columns">
      <p>
        <%= el.render :text %>
      </p>
    </div>
  </div>
<% end %>

The element_view_for helper wraps the content in a div by default. You can change the wrapping tag with the tag argument, or pass false to disable wrapping entirely.

Block Helper Methods

Besides el.render, the block helper provides additional methods for accessing ingredient data.

erb
<%= element_view_for(element) do |el| %>
  <%= el.render :headline %>

  <% if el.has?(:image) %>
    <%= el.render :image %>
  <% end %>

  <span class="date"><%= el.value(:date) %></span>
<% end %>
  • el.render :role - Renders the ingredient's view component (the standard way to display ingredients)
  • el.value(:role) - Returns the raw value without any view component wrapping
  • el.has?(:role) - Returns true if the ingredient has a non-blank value
  • el.ingredient_by_role(:role) - Returns the ingredient record directly

Use el.ingredient_by_role when you need to access ingredient attributes beyond the plain value. For example, a Page ingredient has a page method that returns the actual Alchemy::Page record, which you can use to build custom links.

erb
<%= element_view_for(element) do |el| %>
  <% page_ingredient = el.ingredient_by_role(:linked_page) %>
  <% if page_ingredient&.page %>
    <%= link_to page_ingredient.page.name, show_alchemy_page_path(page_ingredient.page) %>
  <% end %>
<% end %>

Pass Options to the Element Wrapper

You can pass additional arguments to add or change any html attributes of the wrapper.

erb
<%= element_view_for(element, tag: 'li', class: 'red', id: 'my_unique_id') do |el| %>
  ...
<% end %>

Pass Options to the Ingredient View

You can pass options to the ingredient view.

erb
<%= element_view_for(element) do |el| %>
  <%= el.render :image, {size: '200x300'}, class: 'image-large' %>
<% end %>

TIP

Instead of passing the size of an image inline, you should consider using static ingredient settings instead.

The first hash is the options, the second is the html_options passed to the ingredient's wrapper. If you only want to pass html_options, you need to pass an empty hash as the first argument.

erb
<%= element_view_for(element) do |el| %>
  <%= el.render :image, {}, class: 'image-large' %>
<% end %>

Translating Elements

Element and ingredient names are passed through Rails' I18n library. You can translate them in your config/locales language yml file.

yaml
de:
  alchemy:
    element_names:
      contact_form: Kontaktformular
      search: Suche
    ingredient_roles:
      headline: Überschrift

Ingredient roles can also be translated per element. This is useful when the same role name needs different labels depending on the element.

yaml
de:
  alchemy:
    element_names:
      contact_form: Kontaktformular
    ingredient_roles:
      color: Farbe
      contact_form:
        color: Button Farbe

TIP

See the I18n guide for all available translation scopes including hints, deprecation notices, and ingredient groups.

BSD-3 Licensed · Hosting sponsored by netlify