mirror of https://github.com/heroku/12factor.git
Refactor and enhance the Twelve-Factor app
- Updated dependencies in Gemfile.lock for kramdown, parallel, parser, rexml, rubocop-ast, solargraph, and tilt. - Introduced new classes for handling blog posts and authors, including TOML parsing for author data. - Added a new factors configuration file and refactored the factors loading mechanism. - Updated the home view to utilize the new factors structure. - Removed unused dependencies from web.rb and improved helper methods for rendering content.
This commit is contained in:
parent
506d1681dc
commit
6f78f556d2
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"markdown.validate.enabled": true
|
||||
"markdown.validate.enabled": true,
|
||||
"solargraph.commandPath": "./bin/solargraph"
|
||||
}
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -16,8 +16,8 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
jaro_winkler (1.6.0)
|
||||
json (2.10.2)
|
||||
kramdown (2.4.0)
|
||||
rexml
|
||||
kramdown (2.5.1)
|
||||
rexml (>= 3.3.9)
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
language_server-protocol (3.17.0.4)
|
||||
|
@ -30,8 +30,8 @@ GEM
|
|||
racc (~> 1.4)
|
||||
observer (0.1.2)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.4)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
prism (1.4.0)
|
||||
|
@ -49,8 +49,7 @@ GEM
|
|||
regexp_parser (2.10.0)
|
||||
reverse_markdown (3.0.0)
|
||||
nokogiri
|
||||
rexml (3.3.2)
|
||||
strscan
|
||||
rexml (3.4.1)
|
||||
rubocop (1.75.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
|
@ -62,7 +61,7 @@ GEM
|
|||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.44.0)
|
||||
rubocop-ast (1.44.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
ruby-progressbar (1.13.0)
|
||||
|
@ -74,7 +73,7 @@ GEM
|
|||
tilt (~> 2.0)
|
||||
sinatra-partial (1.0.1)
|
||||
sinatra (>= 1.4)
|
||||
solargraph (0.53.4)
|
||||
solargraph (0.54.0)
|
||||
backport (~> 1.2)
|
||||
benchmark
|
||||
bundler (~> 2.0)
|
||||
|
@ -93,13 +92,12 @@ GEM
|
|||
tilt (~> 2.0)
|
||||
yard (~> 0.9, >= 0.9.24)
|
||||
yard-solargraph (~> 0.1)
|
||||
strscan (3.1.0)
|
||||
thin (1.8.2)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
eventmachine (~> 1.0, >= 1.0.4)
|
||||
rack (>= 1, < 3)
|
||||
thor (1.3.2)
|
||||
tilt (2.4.0)
|
||||
tilt (2.6.0)
|
||||
toml-rb (4.0.0)
|
||||
citrus (~> 3.0, > 3.0)
|
||||
racc (~> 1.7)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
factors = [
|
||||
"codebase",
|
||||
"dependencies",
|
||||
"config",
|
||||
"backing-services",
|
||||
"build-release-run",
|
||||
"processes",
|
||||
"port-binding",
|
||||
"concurrency",
|
||||
"disposability",
|
||||
"dev-prod-parity",
|
||||
"logs",
|
||||
"admin-processes",
|
||||
]
|
|
@ -0,0 +1,146 @@
|
|||
require 'toml-rb'
|
||||
require 'front_matter_parser'
|
||||
|
||||
module TwelveFactor
|
||||
|
||||
class Author
|
||||
attr_reader :id
|
||||
attr_reader :name
|
||||
attr_reader :github_username
|
||||
|
||||
def initialize(id, toml)
|
||||
@id = id
|
||||
@toml = toml
|
||||
@name = toml['name']
|
||||
@github_username = toml['github_username']
|
||||
end
|
||||
|
||||
def image_path
|
||||
"/images/bios/#{@id}.jpg"
|
||||
end
|
||||
|
||||
def maintainer?
|
||||
@toml['maintainer']
|
||||
end
|
||||
|
||||
def description
|
||||
@toml['description'] ? MARKDOWN.render(@toml['description']) : ""
|
||||
end
|
||||
end
|
||||
|
||||
class Authors
|
||||
def initialize
|
||||
@authors = TomlRB.load_file("#{ROOT}/blog/authors.toml")
|
||||
end
|
||||
|
||||
def get(id)
|
||||
author = @authors.fetch(id)
|
||||
Author.new(id, author)
|
||||
end
|
||||
end
|
||||
|
||||
AUTHORS = Authors.new
|
||||
|
||||
class BlogPost
|
||||
# A file is a blog post if it has front-matter (i.e. it starts with `---`)
|
||||
def self.is(file)
|
||||
File.read(file, :encoding => 'utf-8').strip.start_with?('---')
|
||||
end
|
||||
|
||||
def self.excerpt(content)
|
||||
# Skip front matter
|
||||
content_without_frontmatter = if content.start_with?('---')
|
||||
content.split('---', 3)[2].to_s.strip
|
||||
else
|
||||
content
|
||||
end
|
||||
|
||||
# Get excerpt (content until the END_EXCERPT marker)
|
||||
if content_without_frontmatter.include?('<!-- END_EXCERPT -->')
|
||||
content_without_frontmatter.split('<!-- END_EXCERPT -->').first.strip
|
||||
else
|
||||
# Fallback to first paragraph if no marker is found
|
||||
content_without_frontmatter.split(/\n\n/).first.to_s.strip
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :content
|
||||
attr_reader :front_matter
|
||||
attr_reader :file_path
|
||||
attr_reader :excerpt
|
||||
attr_reader :slug
|
||||
|
||||
def initialize(file)
|
||||
@file_path = file
|
||||
@slug = File.basename(file, '.md')
|
||||
file_content = File.read(file, :encoding => 'utf-8')
|
||||
# Configure the parser to allow Date objects
|
||||
parser = FrontMatterParser::Parser.new(:md, loader: FrontMatterParser::Loader::Yaml.new(allowlist_classes: [Date]))
|
||||
@front_matter = parser.call(file_content)
|
||||
@content = file_content.split('---', 3)[2].to_s.strip
|
||||
@excerpt = BlogPost.excerpt(file_content)
|
||||
end
|
||||
|
||||
def image_path
|
||||
nil
|
||||
end
|
||||
|
||||
def render
|
||||
MARKDOWN.render(@content)
|
||||
end
|
||||
|
||||
def author
|
||||
AUTHORS.get(@front_matter['author'])
|
||||
end
|
||||
|
||||
def date
|
||||
@front_matter['date'].strftime('%d %b, %Y')
|
||||
end
|
||||
|
||||
def title
|
||||
@front_matter['title']
|
||||
end
|
||||
|
||||
def categories?
|
||||
@front_matter['categories']
|
||||
end
|
||||
|
||||
def categories
|
||||
@front_matter['categories'] || []
|
||||
end
|
||||
|
||||
def featured?
|
||||
@front_matter['featured']
|
||||
end
|
||||
|
||||
def url
|
||||
"/blog/#{@slug}"
|
||||
end
|
||||
end
|
||||
|
||||
class Blog
|
||||
def initialize
|
||||
post_files = Dir["#{ROOT}/blog/*.md"]
|
||||
|
||||
@posts = post_files.filter_map do |file|
|
||||
BlogPost.new(file) if BlogPost.is(file)
|
||||
end
|
||||
|
||||
@posts.sort_by! { |post| post.date }
|
||||
end
|
||||
|
||||
def all
|
||||
@posts
|
||||
end
|
||||
|
||||
def featured
|
||||
@posts.select(&:featured?)
|
||||
end
|
||||
|
||||
def find(slug)
|
||||
@posts.find { |post| post.slug == slug }
|
||||
end
|
||||
end
|
||||
|
||||
BLOG = Blog.new
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
require 'redcarpet'
|
||||
|
||||
module TwelveFactor
|
||||
MARKDOWN = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
|
||||
|
||||
class Document
|
||||
def self.parse(file, excerpt: false)
|
||||
file_content = File.read(file, :encoding => 'utf-8')
|
||||
# Configure the parser to allow Date objects
|
||||
parser = FrontMatterParser::Parser.new(:md, loader: FrontMatterParser::Loader::Yaml.new(allowlist_classes: [Date]))
|
||||
parsed = parser.call(file_content)
|
||||
content = parsed.content.strip
|
||||
excerpt = excerpt ? Document.excerpt(content) : nil
|
||||
Document.new(parsed.front_matter, content, excerpt)
|
||||
end
|
||||
|
||||
|
||||
def self.excerpt(content)
|
||||
# Skip front matter
|
||||
content_without_frontmatter = if content.start_with?('---')
|
||||
content.split('---', 3)[2].to_s.strip
|
||||
else
|
||||
content
|
||||
end
|
||||
|
||||
# Get excerpt (content until the END_EXCERPT marker)
|
||||
if content_without_frontmatter.include?('<!-- END_EXCERPT -->')
|
||||
content_without_frontmatter.split('<!-- END_EXCERPT -->').first.strip
|
||||
else
|
||||
# Fallback to first paragraph if no marker is found
|
||||
content_without_frontmatter.split(/\n\n/).first.to_s.strip
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
attr_reader :front_matter
|
||||
attr_reader :content
|
||||
attr_reader :excerpt
|
||||
|
||||
def initialize(front_matter, content, excerpt)
|
||||
@front_matter = front_matter
|
||||
@content = content
|
||||
@excerpt = excerpt
|
||||
end
|
||||
|
||||
def render
|
||||
MARKDOWN.render(@content)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,95 @@
|
|||
require "toml-rb"
|
||||
|
||||
module TwelveFactor
|
||||
FACTOR_ROOT = "#{ROOT}/content"
|
||||
|
||||
class Factors
|
||||
|
||||
# The factors are loaded from the `content` directory. In that directory,
|
||||
# the factors are stored in subdirectories named after a locale (e.g. `en`,
|
||||
# `fr`, `es`, etc.). Each locale has a localized `toc.md` file and separate
|
||||
# files for each factor.
|
||||
#
|
||||
# The `factors.toml` file contains the list of factors in order.
|
||||
#
|
||||
# This class is used to load the factors and return them as a list of
|
||||
# localized factors.
|
||||
def self.load
|
||||
factors = TomlRB.load_file("#{ROOT}/content/factors.toml")['factors']
|
||||
locales = Dir.glob("#{FACTOR_ROOT}/*").map { |dir| File.basename(dir) }
|
||||
Factors.new(factors, locales)
|
||||
end
|
||||
|
||||
attr_reader :factor_names
|
||||
|
||||
def initialize(factor_names, locales)
|
||||
@factor_names = factor_names
|
||||
@locales = locales
|
||||
end
|
||||
|
||||
def current
|
||||
self[I18n.locale]
|
||||
end
|
||||
|
||||
def [](locale)
|
||||
LocalizedFactors.new(locale)
|
||||
end
|
||||
|
||||
def locales
|
||||
@locales
|
||||
end
|
||||
end
|
||||
|
||||
FACTORS = Factors.load
|
||||
|
||||
class LocalizedFactors
|
||||
FRAGMENTS = [:intro, :who, :background, :toc]
|
||||
|
||||
attr_reader :locale
|
||||
|
||||
def initialize(locale)
|
||||
@locale = locale
|
||||
@root = "#{FACTOR_ROOT}/#{locale}"
|
||||
@documents = {}
|
||||
end
|
||||
|
||||
|
||||
FRAGMENTS.each do |fragment|
|
||||
define_method(fragment) do
|
||||
@documents[fragment] ||= load_document(fragment)
|
||||
end
|
||||
end
|
||||
|
||||
def [](id)
|
||||
return nil unless FACTORS.factor_names.include?(id)
|
||||
|
||||
file = "#{@root}/#{id}.md"
|
||||
TwelveFactor::Document.parse(file)
|
||||
end
|
||||
|
||||
# Returns a list of `LocalizedFactor` objects for each factor in
|
||||
# `FACTORS.factor_names`.
|
||||
def factors
|
||||
FACTORS.factor_names.map do |id|
|
||||
self[id]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def load_document(file)
|
||||
@documents[file] ||= TwelveFactor::Document.parse("#{@root}/#{file}.md")
|
||||
end
|
||||
end
|
||||
|
||||
class LocalizedFactor
|
||||
def initialize(id, file)
|
||||
@id = id
|
||||
@file = file
|
||||
end
|
||||
|
||||
def document
|
||||
TwelveFactor::Document.parse(@file)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -1,146 +1,10 @@
|
|||
require 'toml-rb'
|
||||
require 'front_matter_parser'
|
||||
|
||||
module TwelveFactor
|
||||
ROOT = File.expand_path("..", __dir__)
|
||||
MARKDOWN = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
|
||||
|
||||
class Author
|
||||
attr_reader :id
|
||||
attr_reader :name
|
||||
attr_reader :github_username
|
||||
|
||||
def initialize(id, toml)
|
||||
@id = id
|
||||
@toml = toml
|
||||
@name = toml['name']
|
||||
@github_username = toml['github_username']
|
||||
end
|
||||
|
||||
def image_path
|
||||
"/images/bios/#{@id}.jpg"
|
||||
end
|
||||
|
||||
def maintainer?
|
||||
@toml['maintainer']
|
||||
end
|
||||
|
||||
def description
|
||||
@toml['description'] ? MARKDOWN.render(@toml['description']) : ""
|
||||
end
|
||||
end
|
||||
|
||||
class Authors
|
||||
def initialize
|
||||
@authors = TomlRB.load_file("#{ROOT}/blog/authors.toml")
|
||||
end
|
||||
|
||||
def get(id)
|
||||
author = @authors.fetch(id)
|
||||
Author.new(id, author)
|
||||
end
|
||||
end
|
||||
|
||||
AUTHORS = Authors.new
|
||||
|
||||
class BlogPost
|
||||
# A file is a blog post if it has front-matter (i.e. it starts with `---`)
|
||||
def self.is(file)
|
||||
File.read(file, :encoding => 'utf-8').strip.start_with?('---')
|
||||
end
|
||||
|
||||
def self.excerpt(content)
|
||||
# Skip front matter
|
||||
content_without_frontmatter = if content.start_with?('---')
|
||||
content.split('---', 3)[2].to_s.strip
|
||||
else
|
||||
content
|
||||
end
|
||||
|
||||
# Get excerpt (content until the END_EXCERPT marker)
|
||||
if content_without_frontmatter.include?('<!-- END_EXCERPT -->')
|
||||
content_without_frontmatter.split('<!-- END_EXCERPT -->').first.strip
|
||||
else
|
||||
# Fallback to first paragraph if no marker is found
|
||||
content_without_frontmatter.split(/\n\n/).first.to_s.strip
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :content
|
||||
attr_reader :front_matter
|
||||
attr_reader :file_path
|
||||
attr_reader :excerpt
|
||||
attr_reader :slug
|
||||
|
||||
def initialize(file)
|
||||
@file_path = file
|
||||
@slug = File.basename(file, '.md')
|
||||
file_content = File.read(file, :encoding => 'utf-8')
|
||||
# Configure the parser to allow Date objects
|
||||
parser = FrontMatterParser::Parser.new(:md, loader: FrontMatterParser::Loader::Yaml.new(allowlist_classes: [Date]))
|
||||
@front_matter = parser.call(file_content)
|
||||
@content = file_content.split('---', 3)[2].to_s.strip
|
||||
@excerpt = BlogPost.excerpt(file_content)
|
||||
end
|
||||
|
||||
def image_path
|
||||
nil
|
||||
end
|
||||
|
||||
def render
|
||||
MARKDOWN.render(@content)
|
||||
end
|
||||
|
||||
def author
|
||||
AUTHORS.get(@front_matter['author'])
|
||||
end
|
||||
|
||||
def date
|
||||
@front_matter['date'].strftime('%d %b, %Y')
|
||||
end
|
||||
|
||||
def title
|
||||
@front_matter['title']
|
||||
end
|
||||
|
||||
def categories?
|
||||
@front_matter['categories']
|
||||
end
|
||||
|
||||
def categories
|
||||
@front_matter['categories'] || []
|
||||
end
|
||||
|
||||
def featured?
|
||||
@front_matter['featured']
|
||||
end
|
||||
|
||||
def url
|
||||
"/blog/#{@slug}"
|
||||
end
|
||||
end
|
||||
|
||||
class Blog
|
||||
def initialize
|
||||
post_files = Dir["#{ROOT}/blog/*.md"]
|
||||
|
||||
@posts = post_files.filter_map do |file|
|
||||
BlogPost.new(file) if BlogPost.is(file)
|
||||
end
|
||||
|
||||
@posts.sort_by! { |post| post.date }
|
||||
end
|
||||
|
||||
def all
|
||||
@posts
|
||||
end
|
||||
|
||||
def featured
|
||||
@posts.select(&:featured?)
|
||||
end
|
||||
|
||||
def find(slug)
|
||||
@posts.find { |post| post.slug == slug }
|
||||
end
|
||||
end
|
||||
|
||||
BLOG = Blog.new
|
||||
end
|
||||
|
||||
require_relative "document"
|
||||
require_relative "factors"
|
||||
require_relative "blog"
|
|
@ -1,9 +1,12 @@
|
|||
<section class="abstract">
|
||||
<article><%= render_markdown('intro') %></article>
|
||||
<article><%= render_markdown('background') %></article>
|
||||
<article><%= render_markdown('who') %></article>
|
||||
<article><%= @home.intro.render %></article>
|
||||
<article><%= @home.background.render %></article>
|
||||
<article><%= @home.who.render %></article>
|
||||
</section>
|
||||
|
||||
<section class="concrete">
|
||||
<article><%= render_markdown('toc') %></article>
|
||||
|
||||
<article>
|
||||
<%= @home.toc.render %>
|
||||
</article>
|
||||
</section>
|
||||
|
|
13
web.rb
13
web.rb
|
@ -2,9 +2,6 @@ require 'sinatra'
|
|||
require 'maruku'
|
||||
require 'i18n'
|
||||
require 'rack/ssl-enforcer'
|
||||
require 'toml-rb'
|
||||
require 'redcarpet'
|
||||
require 'front_matter_parser'
|
||||
require 'sinatra/partial'
|
||||
require_relative './lib/twelve_factor'
|
||||
|
||||
|
@ -66,6 +63,7 @@ class App < Sinatra::Base
|
|||
end
|
||||
|
||||
get '/' do
|
||||
@home = TwelveFactor::FACTORS.current
|
||||
erb :home
|
||||
end
|
||||
|
||||
|
@ -142,7 +140,16 @@ class App < Sinatra::Base
|
|||
erb :factor
|
||||
|
||||
end
|
||||
|
||||
helpers do
|
||||
def h(text)
|
||||
Rack::Utils.escape_html(text)
|
||||
end
|
||||
|
||||
def hattr(text)
|
||||
Rack::Utils.escape_path(text)
|
||||
end
|
||||
|
||||
def alternate_links
|
||||
links = [
|
||||
"<link rel=\"alternate\" hreflang=\"x-default\" href=\"#{default_url}\">"
|
||||
|
|
Loading…
Reference in New Issue