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.
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 thebloglayout - Legal pages under
/legal/*get thelegallayout
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
---
Legal pages
---
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
---
Featured images
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)
Footer navigation
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)
Multiple footer sections
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").
How the footer works
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_sectionfrontmatter
Footer layouts
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
- Create a new file in
app/content/layouts/, e.g.,docs.html.erb - Use the
render_layouthelper 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 %>
- Add auto-apply rules in
config/initializers/sitepress.rb:
layouts.layout("docs") do |resource|
resource.request_path.start_with?("/docs")
end
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
draftorexampleflags) - Excludes drafts, examples, and non-HTML resources
- Excludes empty section indexes (e.g.,
/blogwon'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):
last_updatedfrontmatterdatefrontmatter- 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)
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.
After editing railsfast.yml, you must restart the Rails server (bin/dev) for changes to take effect.
Links & social
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 settings
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 helpers
# 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 %>
Progressive footer deep dive
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:
- Blog has publishable posts (
has_blog_content?) - Cornerstone pages with
footer_sectionexist (has_cornerstone_pages?) - Multiple content sections exist (
content_sections.count > 1)
Otherwise, it shows the simple one-line footer.
Footer files
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)
Customizing the 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
- Check that the layout file exists in
app/content/layouts/ - Restart the server after creating new layouts
- 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
- For external URLs, ensure they start with
http://orhttps:// - For asset pipeline images, place them in
app/assets/images/and use the relative path (e.g.,content/hero.jpgforapp/assets/images/content/hero.jpg) - Restart the server after adding new asset directories
Footer not showing cornerstone pages
- Ensure pages have
footer_sectionin frontmatter (e.g.,footer_section: learn) - Make sure pages don't have
draft: trueorexample: true - 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
- Check that
footer_sectionvalue is set correctly in frontmatter - Values are case-sensitive (
learnâ‰Learn) - Restart server after frontmatter changes
Social icons not showing
- Configure
footer.socialsinconfig/railsfast/railsfast.yml - Use lowercase platform names:
github,x,twitter,linkedin, etc. - Restart server after config changes
Signature not appearing in footer
- Configure
footer.signatureinconfig/railsfast/railsfast.yml - Both
textandurlshould be set (url is optional but recommended) - Restart server after config changes
For more advanced usage, see the Sitepress documentation.