Personal, minimal blog template
rails app:template LOCATION='https://perron.railsdesigner.com/library/personal-blog/template.rb'
This template for a minimal and stylish personal blog is designed with Tailwind CSS. It features static pages, like about and contact. And next to that a Content::Post
+ views to showcase all your writings in a clean and minimal visual design.
Just run the template command in your new Perron-powered site to get writing (and publishing) right away.
Template source
gem "tailwindcss-rails" after_bundle do rails_command "tailwindcss:install" rails_command "generate content page" rails_command "generate content post" file "app/assets/tailwind/application.css" do <<~"_" @import "tailwindcss"; @layer base { a:not([class]) { @apply underline; &:hover { @apply no-underline; } } } @layer component { .content { @apply [&_*:not(h1,h2,h3,h4,h5,h6)+*:not(pre,code,li,blockquote>p,svg),blockquote+pre]:mt-5; @apply [&_h1+p,&_h2+p,&_h3+p,&_h4+p,&_h5+p,&_h6+p]:mt-1.5; @apply [&>p,&_li]:text-base [&>p,&_li]:md:text-lg [&>p]:text-gray-950; @apply [&_*+h2,&_*+h3]:mt-8; @apply [&_h2]:font-bold [&_h2]:text-2xl [&_h2]:text-gray-800; @apply [&_h3]:font-semibold [&_h3]:text-xl [&_h3]:text-gray-800; @apply [&_h4]:font-semibold [&_h4]:text-lg [&_h4]:text-gray-800; @apply [&_ul,&_ol]:mt-2 [&_ul]:list-disc [&_ul,&_ol]:list-outside [&_ul,&_ol]:ml-4.5; @apply [&_ol]:list-decimal; @apply [&_pre]:mt-1.25 [&_pre]:px-1.5 [&_pre]:py-2 [&_pre]:sm:px-3 [&_pre]:sm:py-4 [&_pre]:rounded-md [&_pre]:overflow-auto; @apply [&_code]:p-0.5 [&_code]:text-[0.8em] [&_code]:font-medium [&_code]:bg-gray-200/70 [&_code]:rounded-sm; @apply [&_pre>code]:text-base [&_pre>code]:font-normal [&_pre>code]:text-gray-700 [&_pre>code]:bg-transparent; @apply [&_strong]:font-semibold; @apply [&_table]:w-full [&_table]:text-left [&_table]:text-base; @apply [&_thead]:border-b [&_thead]:border-gray-200; @apply [&_th]:px-2 [&_th]:py-1.5 [&_th]:font-semibold [&_th]:text-gray-800; @apply [&_tbody_tr]:border-b [&_tbody_tr]:border-gray-100; @apply [&_tbody_tr:last-child]:border-0; @apply [&_td]:px-2 [&_td]:py-1.5 [&_td]:text-base [&_td]:sm:text-lg [&_td]:text-gray-800; @apply marker:text-gray-500; } } _ end file "app/content/data/README.md" do <<~"_" # Data Perron can consume structured data from YML, JSON, or CSV files, making them available within your templates. This is useful for populating features, team members, or any other repeated data structure. ## Usage To use a data file, you can access it through the `Perron::Site.data` object followed by the basename of the file: ```erb <% Perron::Site.data.features.each do |feature| %> <h4><%= feature.name %></h4> <p><%= feature.description %></p> <% end %> ``` This is a convenient shorthand for `Perron::Data.new("features")`, which can also be used directly: ```ruby <% Perron::Data.new("features").each do |feature| %> <h4><%= feature.name %></h4> <p><%= feature.description %></p> <% end %> ``` ## File Location and Formats By default, Perron looks up `app/content/data/` for files with a `.yml`, `.json`, or `.csv` extension. For a `new("features")` call, it would find `features.yml`, `features.json`, or `features.csv`. You can also provide a full, absolute path to any data file, like `Perron::Data.new("path-to-some-data-file")`. ## Accessing Data The wrapper object provides flexible, read-only access to each record's attributes. Both dot notation and hash-like key access are supported. ```ruby feature.name feature[:name] ``` ## Rendering You can render data collections directly using Rails-like partial rendering. When you call `render` on a data collection, Perron will automatically render a partial for each item. ```erb <%= render Perron::Site.data.features %> ``` This expects a partial at `app/views/content/features/_feature.html.erb` that will be rendered once for each feature in your data file. The individual record is made available as a local variable matching the singular form of the collection name. ```erb <!-- app/views/content/features/_feature.html.erb --> <div class="feature"> <h4><%= feature.name %></h4> <p><%= feature.description %></p> </div> ``` _ end file "app/content/data/social_links.yml" do <<~"_" - name: Social X url: "https://example.com" - name: Social Y url: "https://example.com" - name: Social Z url: "https://example.com" _ end file "app/content/pages/about.erb" do <<~"_" --- nav_label: About title: About me description: description --- <article class="max-w-4xl mx-auto mt-16 px-2"> <h1 class="text-2xl font-sans font-bold text-gray-700 md:text-balance md:text-4xl md:tracking-tight"> <%= @resource.title %> </h1> <p class="mt-5 text-lg text-gray-800 md:text-2xl max-w-prose md:mt-8"> Minim ad et nostrud. Nisi culpa adipisicing nulla cupidatat eu adipisicing non occaecat tempor pariatur sint eiusmod ut pariatur. Et nisi occaecat id tempor qui ex reprehenderit ullamco ullamco duis ad. Amet aliquip velit non pariatur esse aliquip magna. Cillum Lorem proident dolor voluptate fugiat mollit mollit id fugiat dolor minim. </p> <p class="mt-5 text-lg text-gray-800 md:text-2xl max-w-prose md:mt-8"> Est esse ipsum mollit ad sit esse duis commodo voluptate irure commodo proident dolor. Cupidatat officia excepteur Lorem non id ullamco. Pariatur sunt dolore eiusmod Lorem sunt deserunt excepteur veniam occaecat in magna. Exercitation mollit enim eu consectetur adipisicing labore adipisicing eu. </p> </article> _ end file "app/content/pages/contact.erb" do <<~"_" --- nav_label: Contact title: Contact description: description --- <article class="max-w-4xl mx-auto mt-16 px-2"> <h1 class="text-2xl font-sans font-bold text-gray-700 md:text-balance md:text-4xl md:tracking-tight"> <%= @resource.title %> </h1> <p class="mt-5 text-lg text-gray-800 md:text-2xl max-w-prose md:mt-8"> Want to say? Have a question? <%= mail_to "me@example.com", "Send me an email" %> or find me on a platform below. </p> <ul class="flex flex-col gap-y-1 mt-5 text-lg"> <%= render Perron::Site.data.social_links %> </ul> </article> _ end file "app/content/pages/now.erb" do <<~"_" --- nav_label: Now title: What am I doing now? description: description updated_at: 2025-01-01T00:00:00Z doing: - Mastering Blender 3D's geometry nodes system - Writing a book on sustainable design practices - Learning Thai language (currently at B1 level) - Building a hydroponic garden in my apartment --- <article class="max-w-4xl mx-auto mt-16 px-2"> <h1 class="text-2xl font-sans font-bold text-gray-700 md:text-balance md:text-4xl md:tracking-tight"> <%= @resource.title %> </h1> <p class="mt-5 text-lg text-gray-800 md:text-2xl max-w-prose md:mt-8"> These things are keeping me busy day-to-day. Last updated on <%= tag.time @resource.metadata.updated_at.strftime("%Y/%m/%d"), datetime: @resource.metadata.updated_at %> </p> <ul class="flex flex-col gap-y-2 mt-5 text-lg list-inside"> <%= safe_join @resource.metadata.doing.map { tag.li it, class: "list-disc" } %> </ul> </article> _ end file "app/content/pages/root.erb" do <<~"_" --- title: Welcome to my blog description: Welcome to this blog of mine. Here I write about things, stuff and everything. --- <div class="max-w-4xl mx-auto mt-16 px-2 sm:mt-32"> <p class="text-2xl font-normal text-gray-700 md:text-balance md:text-3xl md:tracking-tight"> Hi, my name is Rails Designer. I help companies make their Rails app prettier, more maintainable and a joy to use. </p> <div class="grid grid-cols-6 gap-5 mt-8 text-lg text-gray-900 md:mt-16 md:gap-12"> <p class="col-span-6 sm:col-span-3 first-letter:text-8xl first-letter:float-left first-letter:mr-2 first-letter:mt-2.5"> Minim ad et nostrud. Nisi culpa adipisicing nulla cupidatat eu adipisicing non occaecat tempor pariatur sint eiusmod ut pariatur. Et nisi occaecat id tempor qui ex reprehenderit ullamco ullamco duis ad. Amet aliquip velit non pariatur esse aliquip magna. Cillum Lorem proident dolor voluptate fugiat mollit mollit id fugiat dolor minim. </p> <p class="col-span-6 sm:col-span-3"> Est esse ipsum mollit ad sit esse duis commodo voluptate irure commodo proident dolor. Cupidatat officia excepteur Lorem non id ullamco. Pariatur sunt dolore eiusmod Lorem sunt deserunt excepteur veniam occaecat in magna. Exercitation mollit enim eu consectetur adipisicing labore adipisicing eu. </p> </div> <%= render partial: "shared/latest_post" %> </div> _ end file "app/content/posts/2025-10-01-my-first-post.md" do <<~"_" --- title: My first post on Perron description: Hello world, from Perron! --- Irure nulla pariatur anim mollit labore ad eu amet. Magna ut deserunt officia sunt occaecat reprehenderit veniam voluptate reprehenderit. Sit aliqua cupidatat ea labore consectetur anim amet laborum velit proident pariatur enim. Non mollit laborum dolor. Proident amet cupidatat proident nulla officia duis deserunt mollit dolor sit anim sint consectetur. Proident aliquip eiusmod laborum pariatur proident cupidatat velit minim amet occaecat nisi velit officia sint. Aute fugiat aliquip eiusmod ullamco eu velit ad ad voluptate sunt. Mollit esse magna duis proident consectetur quis duis ipsum eu irure quis aute in veniam. Eu ea tempor nulla ea duis ad incididunt. Non ullamco et nostrud esse ipsum occaecat. Adipisicing sit amet ut sunt occaecat ad Lorem qui cillum. Labore laboris cillum laboris quis non commodo fugiat sit. Adipisicing officia elit sunt ullamco. Laboris exercitation elit cupidatat aliqua excepteur ad laboris ipsum dolore est amet elit velit. Excepteur minim fugiat sit. Amet tempor voluptate fugiat. Veniam minim exercitation laboris excepteur qui ullamco mollit dolor. Lorem minim Lorem pariatur eiusmod quis nulla id magna ullamco laboris ipsum consectetur. Dolor sunt eu mollit magna et id dolor exercitation. Ut voluptate veniam ullamco occaecat minim. Esse Lorem nostrud deserunt exercitation tempor officia amet. Commodo ullamco eiusmod culpa eiusmod velit. Cillum non cupidatat minim sint adipisicing. Excepteur voluptate ullamco commodo eiusmod nulla aute cillum do sunt sunt non laborum est. Voluptate sint quis duis id aute excepteur sunt dolor minim do fugiat ea id irure consequat. Mollit proident exercitation anim duis velit tempor pariatur et deserunt reprehenderit sunt mollit elit exercitation ea. Amet et fugiat est consectetur enim. Amet incididunt deserunt amet dolor ex nostrud nostrud ex. Nulla eu anim id fugiat qui sit id aliquip veniam cupidatat nostrud velit elit. Lorem non sunt sit proident nostrud veniam cupidatat voluptate ex aliqua Lorem minim eiusmod sit. Qui velit exercitation deserunt enim reprehenderit ad. Proident officia duis in velit aliqua voluptate in non cupidatat et quis nulla. Pariatur nostrud ea deserunt proident pariatur velit non occaecat ad quis est. Magna proident ex aliqua dolor esse Lorem labore ex irure do sunt aliquip. Sunt reprehenderit labore deserunt tempor. Cupidatat quis labore aute tempor consequat est ipsum in aliquip laborum. Cillum est dolor dolor aute nisi in laborum laboris. _ end file "app/controllers/content/pages_controller.rb" do <<~"_" class Content::PagesController < ApplicationController def root # Setting `@resource` is required for the the metadata/meta tags generation @resource = Content::Page.root render html: @resource.content, layout: "application" end def show @resource = Content::Page.find(params[:id]) render html: @resource.content, layout: "application" end end _ end file "app/controllers/content/posts_controller.rb" do <<~"_" class Content::PostsController < ApplicationController def index @resources = Content::Post.all end def show @resource = Content::Post.find(params[:id]) end end _ end file "app/models/content/page.rb" do <<~"_" class Content::Page < Perron::Resource delegate :title, :nav_included, to: :metadata end _ end file "app/models/content/post.rb" do <<~"_" class Content::Post < Perron::Resource delegate :title, to: :metadata end _ end file "app/views/content/posts/_post.html.erb" do <<~"_" <%# locals: (post:) %> <li> <%= link_to post, class: "flex items-center justify-between text-lg text-gray-800 underline hover:text-gray-500 hover:no-underline" do %> <%= tag.span post.title %> <%= tag.time post.published_at.strftime("%Y/%m/%d"), datetime: post.published_at, class: "text-base" %> <% end %> </li> _ end file "app/views/content/posts/index.html.erb" do <<~"_" <div class="max-w-4xl mx-auto mt-8 px-2 md:mt-16"> <h1 class="text-2xl font-sans font-bold text-gray-700 md:text-balance md:text-4xl md:tracking-tight"> My writings </h1> <ul class="grid mt-8 gap-y-2"> <%= render @resources %> </ul> </div> _ end file "app/views/content/posts/show.html.erb" do <<~"_" <article class="max-w-4xl mx-auto mt-8 px-2 md:mt-16"> <%= link_to "← View all my writings", posts_path, class: "text-sm font-sans font-normal text-gray-500 transition hover:text-gray-800" %> <h1 class="mt-2 text-2xl font-sans font-bold text-gray-700 md:text-balance md:text-4xl md:tracking-tight"> <%= @resource.title %> </h1> <p class="mt-6 flex items-center gap-x-4 empty:hidden"> <%= tag.span "Published at #{@resource.published_at.strftime("%Y/%m/%d")}", class: "text-sm text-gray-500" if @resource.published_at.present? %> <%= tag.span "Scheduled", class: "inline-block px-2 py-0.5 font-sans text-xs font-medium text-orange-600 bg-orange-100 rounded-md" if @resource.scheduled? %> </p> <div class="content my-5 md:my-8"> <%= markdownify @resource.content %> </div> </article> _ end file "app/views/content/social_links/_social_link.html.erb" do <<~"_" <%# locals: (social_link:) %> <li> <%= link_to social_link.name, social_link.url, target: "_blank", class: "underline text-gray-700 hover:no-underline" %> </li> _ end file "app/views/layouts/application.html.erb" do <<~"_" <!DOCTYPE html> <html> <head> <%= meta_tags %> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= yield :head %> <%= stylesheet_link_tag :app %> </head> <body class="font-serif antialiased"> <%= render partial: "shared/navigation", locals: { items: Content::Page.all.select(&:nav_label) } %> <%= yield %> </body> </html> _ end file "app/views/shared/_latest_post.html.erb" do <<~"_" <%# locals: (latest_post: Content::Post.last) %> <dl class="flex flex-col md:items-end gap-x-4 mt-8 font-sans text-base text-gray-900 md:flex-row md:mt-16 md:text-sm"> <dt class="font-semibold tracking-tight text-gray-800"> <hr class="h-px my-1 border-gray-400 md:border-gray-700"> My last writing </dt> <dd> <%= link_to latest_post.title, latest_post, class: "text-gray-500 transition hover:text-gray-800" %> </dd> </dl> _ end file "app/views/shared/_navigation.html.erb" do <<~"_" <%# locals: (items: []) %> <nav class="sticky top-0 flex items-center justify-between px-2 py-3 max-w-4xl mx-auto font-sans bg-white"> <%= link_to "", root_path, aria: {label: "Go to the homepage"}, class: "block w-14 h-3 bg-gray-800 rounded-xs transition hover:bg-gray-500" %> <ul class="flex items-center gap-x-4 sm:gap-x-6"> <%= render partial: "shared/navigation/item", collection: items %> </ul> </nav> _ end file "app/views/shared/navigation/_item.html.erb" do <<~"_" <li> <%= link_to item.nav_label, item, class: class_names("block px-1 py-2 text-sm font-medium tracking-tight text-gray-800 border-b-2 transparent sm:text-base hover:border-gray-200", {"border-transparent": !current_page?(item), "border-gray-800 hover:border-gray-800": current_page?(item)}) %> </li> _ end end