DOCS LLMs

Blog and content (CMS)

RailsFast includes a simple yet powerful file-based content management system (CMS) powered by Sitepress. This means you can easily create blog posts, legal pages, cornerstone content, and other static pages directly as files in your codebase, so it's perfect for AI agents like Claude Code to create and edit content for you.

There's no database or admin panel. You just create markdown files and they're instantly live.

NOTE

When to restart the server: Content changes (editing markdown files) are hot-reloaded. However, you must restart the server (bin/dev) after: adding new layouts, modifying sitepress.rb, changing railsfast.yml, or adding new cornerstone pages with footer_section.

Quickstart example

You can create a new blog post with a new URL by adding a .html.md file to app/content/pages/blog/. Read the sample file to understand what to put in that file and all available options. Here's a minimal example:

---
title: My first post
---

This is a new **blog post** in my RailsFast blog.

Save the file as my-first-post.html.md. This will automatically "create" the URL /blog/my-first-post. That's it! Just create Markdown files and enjoy!

How it works

Your RailsFast app content lives in app/content/pages/.

The directory structure maps to what URLs look like, so if you have a URL that's blog/my-post, you'll find that Markdown file under app/content/pages/blog/my-post.html.md:

app/content/pages/
├── blog/
│   ├── index.html.erb           → /blog
│   ├── my-post.html.md          → /blog/my-post
│   └── another-post.html.md     → /blog/another-post
├── legal/
│   ├── terms.html.md            → /legal/terms
│   └── privacy.html.md          → /legal/privacy
└── software-licensing.html.md   → /software-licensing (cornerstone content)

Layouts

Layouts control how your content is wrapped and styled. They live in app/content/layouts/:

Layout Purpose Auto-applied to
blog.html.erb Blog posts with date, author, tags, featured image /blog/* posts
cornerstone.html.erb Educational/SEO pages with CTA footer None (specify in frontmatter)
legal.html.erb Legal docs with navigation between terms/privacy /legal/* pages
listing.html.erb Simple wrapper for index pages None (use explicitly)

Auto-applied layouts

You don't need to specify layout: in frontmatter for most pages. RailsFast automatically applies layouts based on the URL path:

  • Blog posts under /blog/* get the blog layout
  • Legal pages under /legal/* get the legal layout

This is configured in config/initializers/sitepress.rb.

Cornerstone content

Cornerstone pages are foundational educational content that lives at root-level URLs (e.g., /software-licensing, /how-it-works). They're great for SEO and establishing topical authority.

---
title: Software licensing explained
subtitle: Adding licenses to software doesn't have to be difficult.
description: Learn what software licensing is and how it works.
layout: cornerstone
image: content/hero.jpg
footer_section: learn
footer_title: Software licensing
nav_order: 1
---

Your content here...

Cornerstone pages include a CTA section at the bottom encouraging signups.

Frontmatter options

Every content file starts with YAML frontmatter between --- markers.

Content visibility flags

These flags control where content appears:

Flag Effect
draft: true Excluded from sitemap, footer, and listings. Use for work-in-progress.
example: true Excluded everywhere. Use for template files in the base repo.
unlisted: true Published and accessible, but hidden from listings (blog index, footer). Still in sitemap.
---
title: Work in Progress
draft: true      # Won't appear anywhere until you remove this
---
---
title: Getting Started Template
example: true    # Template file in base repo, excluded from downstream apps
---
---
title: Secret Landing Page
unlisted: true   # Accessible at /secret-page but not listed anywhere
---

Blog posts

---
title: Your Post Title              # Required - used in <title> and <h1>
subtitle: A brief summary           # Optional - displayed below title
description: SEO meta description   # Optional - for search engines
date: 2025-01-30                    # Optional - displayed in header
author: Author Name                 # Optional - displayed in header
tags:                               # Optional - displayed as badges
  - tag1
  - tag2
image: blog/hero.jpg                # Optional - featured image (see below)
image_alt: Description of image     # Optional - alt text (defaults to title)
image_caption: Photo by Someone     # Optional - caption below image (HTML allowed)
og_image: https://...               # Optional - custom OG image (defaults to image)
---

Cornerstone pages

---
title: Page Title                   # Required
subtitle: Tagline or summary        # Optional - displayed below title
description: SEO meta description   # Optional
layout: cornerstone                 # Required for cornerstone pages
image: content/hero.jpg             # Optional - featured image
image_alt: Description              # Optional - alt text
image_caption: Image credit         # Optional - caption (HTML allowed)
og_image: https://...               # Optional - custom OG image (defaults to image)
footer_section: learn               # Optional - adds to footer navigation
footer_title: Short Label           # Optional - footer link text (defaults to title)
nav_order: 1                        # Optional - sort order in footer
---
---
title: Terms of Service             # Required
description: Legal terms            # Optional
last_updated: 2025-01-30            # Optional - displayed on page
nav_order: 1                        # Optional - sort order in legal nav
---

Both blog and cornerstone layouts support featured images displayed below the title.

Using external URLs

image: https://images.unsplash.com/photo-123?w=1200

Using asset pipeline

Place images in app/assets/images/content/ and reference them:

image: content/my-image.jpg

The image will be served through the Rails asset pipeline with proper cache fingerprinting.

Image captions

Captions support basic HTML for attribution links:

image_caption: 'Photo by <a href="https://unsplash.com/@photographer" target="_blank" rel="noopener">Photographer Name</a> on Unsplash'

Open Graph images

If you set image but not og_image, the featured image is automatically used for Open Graph/Twitter cards. Set og_image explicitly only if you want a different image for social sharing.

Images in content

Add images with captions anywhere in your markdown using HTML <figure> elements:

<figure class="my-8 -mx-4 sm:mx-0">
  <img src="https://example.com/photo.jpg" alt="Description" class="w-full rounded-none sm:rounded-xl shadow-sm">
  <figcaption class="mt-3 text-center text-sm text-gray-500 italic px-4 sm:px-0">
    Caption with optional <a href="...">links</a>
  </figcaption>
</figure>

The classes provide:

  • Full-bleed on mobile (-mx-4, rounded-none)
  • Rounded corners on larger screens (sm:rounded-xl)
  • Subtle shadow (shadow-sm)
  • Styled caption (text-gray-500 italic)

Cornerstone pages can be added to the site footer by setting footer_section in frontmatter:

footer_section: learn    # Section name (becomes column title)
footer_title: Licensing  # Short label for link (defaults to title)
nav_order: 1             # Sort order within section (lower = first)

You can create multiple footer sections by using different footer_section values:

# Page 1: software-licensing.html.md
footer_section: learn
footer_title: Software licensing
nav_order: 1

# Page 2: api-reference.html.md
footer_section: resources
footer_title: API Reference
nav_order: 1

# Page 3: tutorials.html.md
footer_section: resources
footer_title: Tutorials
nav_order: 2

This creates two footer columns: "Learn" and "Resources". The section name is automatically titleized (e.g., learn → "Learn", resources → "Resources").

RailsFast uses a progressive footer system that automatically adapts:

Content Available Footer Style
No blog posts, no cornerstone pages Simple one-line footer
Blog posts OR cornerstone pages exist Multi-column footer

The footer automatically detects content and expands when you add:

  • Blog posts (without draft: true)
  • Cornerstone pages with footer_section frontmatter

Simple footer (_footer_simple.html.erb):

[Logo + "built with RailsFast"] | [Home Pricing Docs Privacy Terms] | [© 2026 AppName + signature]

Multi-column footer (_footer_full.html.erb):

  • Brand column: Logo, tagline, social icons, "built with RailsFast"
  • Dynamic columns: Product, cornerstone sections (Learn, Resources, etc.), Legal
  • Bottom bar: Copyright + signature

The layout adapts based on the number of sections:

  • ≤4 sections: Uses flexbox with justify-between (evenly spaced)
  • >4 sections: Uses CSS multi-column layout (flows naturally like a newspaper)

Creating a new layout

  1. Create a new file in app/content/layouts/, e.g., docs.html.erb
  2. Use the render_layout helper to wrap in the application layout:
<%# Set meta tags %>
<% title current_page.data.fetch("title", "Docs") %>
<% description current_page.data["description"] if current_page.data["description"] %>
<%
  meta_image = current_page.data["og_image"] || current_page.data["image"]
  meta_image_url = page_image_url(meta_image) if meta_image
%>
<% if meta_image_url %>
  <% set_meta_tags og: { image: meta_image_url }, twitter: { image: meta_image_url } %>
<% end %>

<%= render_layout "application" do %>
  <div class="max-w-4xl mx-auto px-4 py-16">
    <h1 class="text-4xl font-bold">
      <%= current_page.data.fetch("title", "Untitled") %>
    </h1>

    <div class="prose prose-lg mt-8">
      <%= yield %>
    </div>
  </div>
<% end %>
  1. Add auto-apply rules in config/initializers/sitepress.rb:
layouts.layout("docs") do |resource|
  resource.request_path.start_with?("/docs")
end
WARNING

After creating a new layout or modifying sitepress.rb, you must restart the server (bin/dev). Layout changes aren't hot-reloaded.

Styling content

Content inside the <%= yield %> block is rendered as HTML from your markdown. Use Tailwind's Typography plugin (already installed and configured) for beautiful prose styling:

<div class="prose prose-lg prose-gray">
  <%= yield %>
</div>

The typography plugin is already configured in app/assets/tailwind/application.css:

@plugin "@tailwindcss/typography";
@source "../../content/**/*.*";

Common Tailwind prose customizations

<div class="prose prose-lg prose-gray max-w-none
            prose-headings:font-bold
            prose-a:text-blue-600
            hover:prose-a:text-blue-800
            prose-code:before:content-none
            prose-code:after:content-none">
  <%= yield %>
</div>

For example, the prose-code:before:content-none and prose-code:after:content-none classes remove the backticks that Typography adds around inline code by default.

SEO integration

All the content you generate under this system is SEO-ready by default in RailsFast.

All layouts integrate with the meta-tags gem for SEO titles, descriptions, OG images. The SEO helpers for setting title, description, and OG images work the same as in regular Rails views:

<% title current_page.data.fetch("title", "Default") %>
<% description current_page.data["description"] if current_page.data["description"] %>
<% if current_page.data["og_image"] %>
  <% set_meta_tags og: { image: current_page.data["og_image"] } %>
<% end %>

If no og_image is specified in frontmatter, the default from config/railsfast/railsfast.yml is used automatically.

Sitemap

The sitemap is generated intelligently at /sitemaps/sitemap.xml. It automatically adapts to your content structure, similar to how the footer works.

Smart inclusion rules

The sitemap automatically:

  • Includes all publishable content (HTML pages without draft or example flags)
  • Excludes drafts, examples, and non-HTML resources
  • Excludes empty section indexes (e.g., /blog won't appear if there are no published blog posts)

How sections work

A section is a folder containing content pages (e.g., blog/, guides/, docs/). The sitemap handles sections intelligently:

Scenario What appears in sitemap
Section folder with index + publishable posts Index page + all posts
Section folder with index but only draft/example posts Nothing (index excluded)
Section folder with posts but no index page Only the posts (no index)
Empty section folder Nothing

Example: If you create app/content/pages/guides/ with:

  • index.html.erb (the listing page)
  • getting-started.html.md (a guide)
  • advanced-tips.html.md (draft: true)

The sitemap will include /guides and /guides/getting-started, but not /guides/advanced-tips.

If you later mark getting-started.html.md as draft: true, the /guides index will also disappear from the sitemap (no point indexing an empty listing page).

Priority and change frequency

Priority and change frequency are assigned based on page type:

Page Type Priority Change Frequency
Homepage (/) 1.0 weekly
Top-level pages (/blog, /about, cornerstone) 0.8 weekly
Regular content (blog posts, guides) 0.6 weekly
Legal pages (/legal/*) 0.3 yearly

The lastmod date comes from (in order of preference):

  1. last_updated frontmatter
  2. date frontmatter
  3. File modification time

Regenerating the sitemap

Regenerate after adding new content (automatic in production):

bin/rails sitemap:refresh:no_ping  # Development (no search engine ping)
bin/rails sitemap:refresh          # Production (pings search engines)
NOTE

The sitemap uses the same publishable? logic as the footer. Content with draft: true or example: true is excluded from both. See Content visibility flags for details on these frontmatter options.

Configuration

All CMS and footer settings are in config/railsfast/railsfast.yml.

WARNING

After editing railsfast.yml, you must restart the Rails server (bin/dev) for changes to take effect.

These are app-wide settings used by the footer:

default: &default
  # ... other settings ...

  # External documentation URL (leave blank to hide from footer)
  docs_url: "https://yourapp.com/docs"

  # Social media links - shown in footer
  # Supports: github, x, linkedin, discord, youtube, mastodon, bluesky
  socials:
    - platform: github
      url: "https://github.com/yourusername"
    - platform: x
      url: "https://x.com/yourusername"

The footer displays social icons with hover effects. Supported platforms have built-in SVG icons.

Footer-specific settings:

default: &default
  # ... other settings ...

  footer:
    # Tagline shown under logo in multi-column footer
    # Leave blank to use truncated default_page_description
    tagline: "Software licensing in 15 minutes."

    # Signature shown in footer bottom bar (leave blank to hide)
    signature:
      prefix: "by"           # Optional: "by", "built by", "made by", etc.
      text: "@yourhandle"
      url: "https://yoursite.com"

Blog settings

default: &default
  # ... other settings ...

  blog:
    title: "Blog"
    description: "Thoughts, tutorials, and updates from our team."
    back_link_text: "Back to all posts"

These are used by the blog layout and blog index page.

Cornerstone CTA settings

The cornerstone layout includes a CTA section at the bottom:

default: &default
  # ... other settings ...

  cornerstone:
    cta_enabled: true                    # Set to false to hide the CTA
    cta_title: "Ready to get started?"
    cta_subtitle: "Sign up today and see the difference."
    cta_button_text: "Get started"
    cta_path: /signup                    # Optional: defaults to new_user_registration_path

URL Redirects

The /legal path redirects to /legal/terms by default. Add similar redirects in config/routes.rb:

get "/legal", to: redirect("/legal/terms")

# Sitepress must always be at the end (catch-all)
sitepress_pages

Markdown features

The markdown renderer (via markdown-rails gem) has these features enabled:

  • Fenced code blocks with syntax highlighting
  • Tables
  • Autolinks
  • Strikethrough (~~deleted~~)
  • Footnotes ([^1])
  • Superscript (^superscript^)
  • Raw HTML passthrough (for <figure> elements, etc.)

Configure in config/initializers/markdown.rb.

Querying content programmatically

Access content pages in Ruby code:

# Get all blog posts, sorted by date
posts = Sitepress.site.resources.glob("blog/*.html*")
  .reject { |r| r.request_path == "/blog" }  # Exclude index
  .sort_by { |r| r.data["date"] || Date.new(1970) }
  .reverse

# Access page data
posts.each do |post|
  puts post.request_path        # "/blog/my-post"
  puts post.data["title"]       # "My Post Title"
  puts post.data["date"]        # Date object
end

# Get cornerstone pages for footer
learn_pages = Sitepress.site.resources.glob("*.html*")
  .select { |p| p.data["footer_section"] == "learn" }
  .sort_by { |p| [p.data["nav_order"] || 100, p.data["title"]] }

This is how the blog index page lists all posts and how the footer dynamically includes cornerstone pages.

Helper methods

The PageHelper module (app/content/helpers/page_helper.rb) provides helpers for working with content.

Image helpers

# Resolve image URLs (external or asset pipeline)
page_image_url("https://example.com/image.jpg")  # Returns as-is
page_image_url("content/hero.jpg")               # Returns asset pipeline URL
# Link to a page using its title
link_to_page(page)

# Link with active class if current
link_to_if_current("Home", page, active_class: "text-blue-600")

Layout helpers

# Render nested layouts
render_layout("application") { ... }

Content discovery helpers

These helpers power the footer and listings:

# Check if Sitepress is available (works from any context)
sitepress_available?

# Check content visibility
publishable?(resource)    # Not draft, not example
listable?(resource)       # Publishable and not unlisted

# Blog helpers
has_blog_content?         # True if blog has real posts
blog_posts                # All publishable blog posts, newest first

# Footer helpers
footer_cornerstone_pages  # Pages with footer_section, sorted by nav_order
has_cornerstone_pages?    # True if cornerstone pages exist
show_expanded_footer?     # True if multi-column footer should show

# Section helpers
content_sections          # All sections with publishable content
section_content("blog")   # Get content for a specific section
section_has_content?("blog")  # Check if section has content

Using helpers in views

All helpers are available in both Sitepress layouts and regular Rails views:

<%# In any view or layout %>
<% if has_blog_content? %>
  <a href="/blog">Read our blog</a>
<% end %>

<% footer_cornerstone_pages.each do |page| %>
  <%= link_to page.data["title"], page.request_path %>
<% end %>

The footer system automatically adapts based on your content:

Detection logic

The footer shows the expanded multi-column layout when ANY of these are true:

  1. Blog has publishable posts (has_blog_content?)
  2. Cornerstone pages with footer_section exist (has_cornerstone_pages?)
  3. Multiple content sections exist (content_sections.count > 1)

Otherwise, it shows the simple one-line footer.

app/views/layouts/partials/
├── _footer.html.erb         # Conditional wrapper (chooses which to render)
├── _footer_simple.html.erb  # One-line footer
└── _footer_full.html.erb    # Multi-column footer

app/views/shared/
├── _footer_bottom.html.erb  # Copyright bar (used by full footer)
└── _footer_social_icons.html.erb  # Social icons (used by full footer)

To always show the simple footer: Edit _footer.html.erb to always render footer_simple.

To add more Product links: Edit _footer_full.html.erb and add links to the Product column.

To change column order: Edit the column order in _footer_full.html.erb.

To add a new fixed column (like "Company"): Add a new column div in _footer_full.html.erb between the dynamic sections and Legal.


Common pitfalls

Page renders without styling

Make sure your layout uses render_layout "application" to wrap content in the main Rails layout (navbar, footer, CSS):

<%= render_layout "application" do %>
  <!-- your content here -->
<% end %>

Layout not being applied

  1. Check that the layout file exists in app/content/layouts/
  2. Restart the server after creating new layouts
  3. Verify the auto-apply rule in config/initializers/sitepress.rb

Code blocks show as inline code

Ensure config/initializers/markdown.rb exists with fenced code blocks enabled. This file is required for proper code block rendering.

Featured image not loading

  1. For external URLs, ensure they start with http:// or https://
  2. For asset pipeline images, place them in app/assets/images/ and use the relative path (e.g., content/hero.jpg for app/assets/images/content/hero.jpg)
  3. Restart the server after adding new asset directories

Footer not showing cornerstone pages

  1. Ensure pages have footer_section in frontmatter (e.g., footer_section: learn)
  2. Make sure pages don't have draft: true or example: true
  3. Restart the server after adding new cornerstone pages

Footer showing simple layout when it should be multi-column

The multi-column footer requires at least one of:

  • Blog posts without draft: true
  • Cornerstone pages with footer_section

Check that your content doesn't have draft: true or example: true set.

Footer section not appearing

  1. Check that footer_section value is set correctly in frontmatter
  2. Values are case-sensitive (learn ≠ Learn)
  3. Restart server after frontmatter changes

Social icons not showing

  1. Configure footer.socials in config/railsfast/railsfast.yml
  2. Use lowercase platform names: github, x, twitter, linkedin, etc.
  3. Restart server after config changes

Signature not appearing in footer

  1. Configure footer.signature in config/railsfast/railsfast.yml
  2. Both text and url should be set (url is optional but recommended)
  3. Restart server after config changes

For more advanced usage, see the Sitepress documentation.