mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Load site and projects from TOML files
This commit is contained in:
parent
a3dc9b55f8
commit
848054f941
9 changed files with 551 additions and 56 deletions
10
Readme.md
10
Readme.md
|
|
@ -10,6 +10,7 @@ This repository is now a single integrated Ruby project. The legacy Swift genera
|
|||
- Build tasks: `bake.rb`
|
||||
- CLI and utilities: `bin/`
|
||||
- Tests: `spec/`
|
||||
- Config: `site.toml` and `projects.toml`
|
||||
- Content: `posts/` and `public/`
|
||||
- Output: `www/`
|
||||
|
||||
|
|
@ -39,6 +40,15 @@ rbenv exec bundle exec bake debug # build for http://localhost:8000
|
|||
rbenv exec bundle exec bake serve # serve www/ locally
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Site metadata and project data are configured with TOML files at the repository root:
|
||||
|
||||
- `site.toml`: site identity, default scripts/styles, and `projects_plugin` assets.
|
||||
- `projects.toml`: project listing entries using `[[projects]]`.
|
||||
|
||||
`Pressa.create_site` loads both files from the provided `source_path` and still supports URL overrides for `debug`, `beta`, and `release` builds.
|
||||
|
||||
Other targets:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
2
bake.rb
2
bake.rb
|
|
@ -85,7 +85,7 @@ def build(url)
|
|||
require_relative 'lib/pressa'
|
||||
|
||||
puts "Building site for #{url}..."
|
||||
site = Pressa.create_site(url_override: url)
|
||||
site = Pressa.create_site(source_path: '.', url_override: url)
|
||||
generator = Pressa::SiteGenerator.new(site:)
|
||||
generator.generate(source_path: '.', target_path: 'www')
|
||||
puts "Site built successfully in www/"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ target_path = ARGV[1]
|
|||
site_url = ARGV[2]
|
||||
|
||||
begin
|
||||
site = Pressa.create_site(url_override: site_url)
|
||||
site = Pressa.create_site(source_path:, url_override: site_url)
|
||||
generator = Pressa::SiteGenerator.new(site:)
|
||||
generator.generate(source_path:, target_path:)
|
||||
puts "Site generated successfully!"
|
||||
|
|
|
|||
157
lib/config/loader.rb
Normal file
157
lib/config/loader.rb
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
require_relative '../site'
|
||||
require_relative '../posts/plugin'
|
||||
require_relative '../projects/plugin'
|
||||
require_relative '../utils/markdown_renderer'
|
||||
require_relative 'simple_toml'
|
||||
|
||||
module Pressa
|
||||
module Config
|
||||
class ValidationError < StandardError; end
|
||||
|
||||
class Loader
|
||||
REQUIRED_SITE_KEYS = %w[author email title description url].freeze
|
||||
REQUIRED_PROJECT_KEYS = %w[name title description url].freeze
|
||||
|
||||
def initialize(source_path:)
|
||||
@source_path = source_path
|
||||
end
|
||||
|
||||
def build_site(url_override: nil)
|
||||
site_config = load_toml('site.toml')
|
||||
projects_config = load_toml('projects.toml')
|
||||
|
||||
validate_required!(site_config, REQUIRED_SITE_KEYS, context: 'site.toml')
|
||||
|
||||
site_url = url_override || site_config['url']
|
||||
projects_plugin = hash_or_empty(site_config['projects_plugin'], 'site.toml projects_plugin')
|
||||
|
||||
projects = build_projects(projects_config)
|
||||
|
||||
Site.new(
|
||||
author: site_config['author'],
|
||||
email: site_config['email'],
|
||||
title: site_config['title'],
|
||||
description: site_config['description'],
|
||||
url: site_url,
|
||||
image_url: normalize_image_url(site_config['image_url'], site_url),
|
||||
scripts: build_scripts(site_config['scripts'], context: 'site.toml scripts'),
|
||||
styles: build_styles(site_config['styles'], context: 'site.toml styles'),
|
||||
plugins: [
|
||||
Posts::Plugin.new,
|
||||
Projects::Plugin.new(
|
||||
projects:,
|
||||
scripts: build_scripts(projects_plugin['scripts'], context: 'site.toml projects_plugin.scripts'),
|
||||
styles: build_styles(projects_plugin['styles'], context: 'site.toml projects_plugin.styles')
|
||||
)
|
||||
],
|
||||
renderers: [
|
||||
Utils::MarkdownRenderer.new
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_toml(filename)
|
||||
path = File.join(@source_path, filename)
|
||||
SimpleToml.load_file(path)
|
||||
rescue ParseError => e
|
||||
raise ValidationError, e.message
|
||||
end
|
||||
|
||||
def build_projects(projects_config)
|
||||
projects = projects_config['projects']
|
||||
raise ValidationError, "Missing required top-level array 'projects' in projects.toml" unless projects
|
||||
raise ValidationError, "Expected 'projects' to be an array in projects.toml" unless projects.is_a?(Array)
|
||||
|
||||
projects.map.with_index do |project, index|
|
||||
unless project.is_a?(Hash)
|
||||
raise ValidationError, "Project entry #{index + 1} must be a table in projects.toml"
|
||||
end
|
||||
|
||||
validate_required!(project, REQUIRED_PROJECT_KEYS, context: "projects.toml project ##{index + 1}")
|
||||
|
||||
Projects::Project.new(
|
||||
name: project['name'],
|
||||
title: project['title'],
|
||||
description: project['description'],
|
||||
url: project['url']
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_required!(hash, keys, context:)
|
||||
missing = keys.reject do |key|
|
||||
hash[key].is_a?(String) && !hash[key].strip.empty?
|
||||
end
|
||||
|
||||
return if missing.empty?
|
||||
|
||||
raise ValidationError, "Missing required #{context} keys: #{missing.join(', ')}"
|
||||
end
|
||||
|
||||
def hash_or_empty(value, context)
|
||||
return {} if value.nil?
|
||||
return value if value.is_a?(Hash)
|
||||
|
||||
raise ValidationError, "Expected #{context} to be a table"
|
||||
end
|
||||
|
||||
def build_scripts(value, context:)
|
||||
entries = array_or_empty(value, context)
|
||||
|
||||
entries.map.with_index do |item, index|
|
||||
case item
|
||||
when String
|
||||
Script.new(src: item, defer: true)
|
||||
when Hash
|
||||
src = item['src']
|
||||
raise ValidationError, "Expected #{context}[#{index}].src to be a String" unless src.is_a?(String) && !src.empty?
|
||||
|
||||
defer = item.key?('defer') ? item['defer'] : true
|
||||
unless [true, false].include?(defer)
|
||||
raise ValidationError, "Expected #{context}[#{index}].defer to be a Boolean"
|
||||
end
|
||||
|
||||
Script.new(src:, defer:)
|
||||
else
|
||||
raise ValidationError, "Expected #{context}[#{index}] to be a String or table"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_styles(value, context:)
|
||||
entries = array_or_empty(value, context)
|
||||
|
||||
entries.map.with_index do |item, index|
|
||||
case item
|
||||
when String
|
||||
Stylesheet.new(href: item)
|
||||
when Hash
|
||||
href = item['href']
|
||||
raise ValidationError, "Expected #{context}[#{index}].href to be a String" unless href.is_a?(String) && !href.empty?
|
||||
|
||||
Stylesheet.new(href:)
|
||||
else
|
||||
raise ValidationError, "Expected #{context}[#{index}] to be a String or table"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def array_or_empty(value, context)
|
||||
return [] if value.nil?
|
||||
return value if value.is_a?(Array)
|
||||
|
||||
raise ValidationError, "Expected #{context} to be an array"
|
||||
end
|
||||
|
||||
def normalize_image_url(value, site_url)
|
||||
return nil if value.nil?
|
||||
return value if value.start_with?('http://', 'https://')
|
||||
|
||||
normalized = value.start_with?('/') ? value : "/#{value}"
|
||||
"#{site_url}#{normalized}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
224
lib/config/simple_toml.rb
Normal file
224
lib/config/simple_toml.rb
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
require 'json'
|
||||
|
||||
module Pressa
|
||||
module Config
|
||||
class ParseError < StandardError; end
|
||||
|
||||
class SimpleToml
|
||||
def self.load_file(path)
|
||||
new.parse(File.read(path))
|
||||
rescue Errno::ENOENT
|
||||
raise ParseError, "Config file not found: #{path}"
|
||||
end
|
||||
|
||||
def parse(content)
|
||||
root = {}
|
||||
current_table = root
|
||||
lines = content.each_line.to_a
|
||||
|
||||
line_index = 0
|
||||
while line_index < lines.length
|
||||
line = lines[line_index]
|
||||
line_number = line_index + 1
|
||||
source = strip_comments(line).strip
|
||||
if source.empty?
|
||||
line_index += 1
|
||||
next
|
||||
end
|
||||
|
||||
if source =~ /\A\[\[(.+)\]\]\z/
|
||||
current_table = start_array_table(root, Regexp.last_match(1), line_number)
|
||||
line_index += 1
|
||||
next
|
||||
end
|
||||
|
||||
if source =~ /\A\[(.+)\]\z/
|
||||
current_table = start_table(root, Regexp.last_match(1), line_number)
|
||||
line_index += 1
|
||||
next
|
||||
end
|
||||
|
||||
key, raw_value = parse_assignment(source, line_number)
|
||||
while needs_continuation?(raw_value)
|
||||
line_index += 1
|
||||
raise ParseError, "Unterminated value for key '#{key}' at line #{line_number}" if line_index >= lines.length
|
||||
|
||||
continuation = strip_comments(lines[line_index]).strip
|
||||
next if continuation.empty?
|
||||
|
||||
raw_value = "#{raw_value} #{continuation}"
|
||||
end
|
||||
|
||||
if current_table.key?(key)
|
||||
raise ParseError, "Duplicate key '#{key}' at line #{line_number}"
|
||||
end
|
||||
|
||||
current_table[key] = parse_value(raw_value, line_number)
|
||||
line_index += 1
|
||||
end
|
||||
|
||||
root
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def start_array_table(root, raw_path, line_number)
|
||||
keys = parse_path(raw_path, line_number)
|
||||
parent = ensure_path(root, keys[0..-2], line_number)
|
||||
table_name = keys.last
|
||||
|
||||
parent[table_name] ||= []
|
||||
array = parent[table_name]
|
||||
unless array.is_a?(Array)
|
||||
raise ParseError, "Expected array for '[[#{raw_path}]]' at line #{line_number}"
|
||||
end
|
||||
|
||||
table = {}
|
||||
array << table
|
||||
table
|
||||
end
|
||||
|
||||
def start_table(root, raw_path, line_number)
|
||||
keys = parse_path(raw_path, line_number)
|
||||
ensure_path(root, keys, line_number)
|
||||
end
|
||||
|
||||
def ensure_path(root, keys, line_number)
|
||||
cursor = root
|
||||
|
||||
keys.each do |key|
|
||||
cursor[key] ||= {}
|
||||
unless cursor[key].is_a?(Hash)
|
||||
raise ParseError, "Expected table path '#{keys.join('.')}' at line #{line_number}"
|
||||
end
|
||||
|
||||
cursor = cursor[key]
|
||||
end
|
||||
|
||||
cursor
|
||||
end
|
||||
|
||||
def parse_path(raw_path, line_number)
|
||||
keys = raw_path.split('.').map(&:strip)
|
||||
if keys.empty? || keys.any? { |part| part.empty? || part !~ /\A[A-Za-z0-9_]+\z/ }
|
||||
raise ParseError, "Invalid table path '#{raw_path}' at line #{line_number}"
|
||||
end
|
||||
keys
|
||||
end
|
||||
|
||||
def parse_assignment(source, line_number)
|
||||
separator = index_of_unquoted(source, '=')
|
||||
raise ParseError, "Invalid assignment at line #{line_number}" unless separator
|
||||
|
||||
key = source[0...separator].strip
|
||||
value = source[(separator + 1)..].strip
|
||||
|
||||
if key.empty? || key !~ /\A[A-Za-z0-9_]+\z/
|
||||
raise ParseError, "Invalid key '#{key}' at line #{line_number}"
|
||||
end
|
||||
raise ParseError, "Missing value for key '#{key}' at line #{line_number}" if value.empty?
|
||||
|
||||
[key, value]
|
||||
end
|
||||
|
||||
def parse_value(raw_value, line_number)
|
||||
JSON.parse(raw_value)
|
||||
rescue JSON::ParserError
|
||||
raise ParseError, "Unsupported TOML value '#{raw_value}' at line #{line_number}"
|
||||
end
|
||||
|
||||
def needs_continuation?(source)
|
||||
in_string = false
|
||||
escaped = false
|
||||
depth = 0
|
||||
|
||||
source.each_char do |char|
|
||||
if in_string
|
||||
if escaped
|
||||
escaped = false
|
||||
elsif char == '\\'
|
||||
escaped = true
|
||||
elsif char == '"'
|
||||
in_string = false
|
||||
end
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
case char
|
||||
when '"'
|
||||
in_string = true
|
||||
when '[', '{'
|
||||
depth += 1
|
||||
when ']', '}'
|
||||
depth -= 1
|
||||
end
|
||||
end
|
||||
|
||||
in_string || depth.positive?
|
||||
end
|
||||
|
||||
def strip_comments(line)
|
||||
output = +''
|
||||
in_string = false
|
||||
escaped = false
|
||||
|
||||
line.each_char do |char|
|
||||
if in_string
|
||||
output << char
|
||||
|
||||
if escaped
|
||||
escaped = false
|
||||
elsif char == '\\'
|
||||
escaped = true
|
||||
elsif char == '"'
|
||||
in_string = false
|
||||
end
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
case char
|
||||
when '"'
|
||||
in_string = true
|
||||
output << char
|
||||
when '#'
|
||||
break
|
||||
else
|
||||
output << char
|
||||
end
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def index_of_unquoted(source, target)
|
||||
in_string = false
|
||||
escaped = false
|
||||
|
||||
source.each_char.with_index do |char, index|
|
||||
if in_string
|
||||
if escaped
|
||||
escaped = false
|
||||
elsif char == '\\'
|
||||
escaped = true
|
||||
elsif char == '"'
|
||||
in_string = false
|
||||
end
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
if char == '"'
|
||||
in_string = true
|
||||
next
|
||||
end
|
||||
|
||||
return index if char == target
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,61 +4,11 @@ require_relative 'plugin'
|
|||
require_relative 'posts/plugin'
|
||||
require_relative 'projects/plugin'
|
||||
require_relative 'utils/markdown_renderer'
|
||||
require_relative 'config/loader'
|
||||
|
||||
module Pressa
|
||||
def self.create_site(url_override: nil)
|
||||
url = url_override || 'https://samhuri.net'
|
||||
|
||||
build_project = lambda do |name, title, description|
|
||||
Projects::Project.new(
|
||||
name:,
|
||||
title:,
|
||||
description:,
|
||||
url: "https://github.com/samsonjs/#{title}"
|
||||
)
|
||||
end
|
||||
|
||||
projects = [
|
||||
build_project.call('samhuri.net', 'samhuri.net', 'this site'),
|
||||
build_project.call('bin', 'bin', 'my collection of scripts in ~/bin'),
|
||||
build_project.call('config', 'config', 'important dot files (zsh, emacs, vim, screen)'),
|
||||
build_project.call('compiler', 'compiler', 'a compiler targeting x86 in Ruby'),
|
||||
build_project.call('lake', 'lake', 'a simple implementation of Scheme in C'),
|
||||
build_project.call('AsyncMonitor', 'AsyncMonitor', 'easily monitor async sequences using Swift concurrency'),
|
||||
build_project.call('NotificationSmuggler', 'NotificationSmuggler', 'embed strongly-typed values in notifications on Apple platforms'),
|
||||
build_project.call('strftime', 'strftime', 'strftime for JavaScript'),
|
||||
build_project.call('format', 'format', 'printf for JavaScript'),
|
||||
build_project.call('gitter', 'gitter', 'a GitHub client for Node (v3 API)'),
|
||||
build_project.call('cheat.el', 'cheat.el', 'cheat from emacs')
|
||||
]
|
||||
|
||||
project_scripts = [
|
||||
Script.new(src: 'https://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js', defer: true),
|
||||
Script.new(src: 'js/gitter.js', defer: true),
|
||||
Script.new(src: 'js/store.js', defer: true),
|
||||
Script.new(src: 'js/projects.js', defer: true)
|
||||
]
|
||||
|
||||
Site.new(
|
||||
author: 'Sami Samhuri',
|
||||
email: 'sami@samhuri.net',
|
||||
title: 'samhuri.net',
|
||||
description: "Sami Samhuri's blog about programming, mainly about iOS and Ruby and Rails these days.",
|
||||
url:,
|
||||
image_url: "#{url}/images/me.jpg",
|
||||
scripts: [],
|
||||
styles: [
|
||||
Stylesheet.new(href: 'css/normalize.css'),
|
||||
Stylesheet.new(href: 'css/style.css'),
|
||||
Stylesheet.new(href: 'css/syntax.css')
|
||||
],
|
||||
plugins: [
|
||||
Posts::Plugin.new,
|
||||
Projects::Plugin.new(projects:, scripts: project_scripts, styles: [])
|
||||
],
|
||||
renderers: [
|
||||
Utils::MarkdownRenderer.new
|
||||
]
|
||||
)
|
||||
def self.create_site(source_path: '.', url_override: nil)
|
||||
loader = Config::Loader.new(source_path:)
|
||||
loader.build_site(url_override:)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
65
projects.toml
Normal file
65
projects.toml
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
[[projects]]
|
||||
name = "samhuri.net"
|
||||
title = "samhuri.net"
|
||||
description = "this site"
|
||||
url = "https://github.com/samsonjs/samhuri.net"
|
||||
|
||||
[[projects]]
|
||||
name = "bin"
|
||||
title = "bin"
|
||||
description = "my collection of scripts in ~/bin"
|
||||
url = "https://github.com/samsonjs/bin"
|
||||
|
||||
[[projects]]
|
||||
name = "config"
|
||||
title = "config"
|
||||
description = "important dot files (zsh, emacs, vim, screen)"
|
||||
url = "https://github.com/samsonjs/config"
|
||||
|
||||
[[projects]]
|
||||
name = "compiler"
|
||||
title = "compiler"
|
||||
description = "a compiler targeting x86 in Ruby"
|
||||
url = "https://github.com/samsonjs/compiler"
|
||||
|
||||
[[projects]]
|
||||
name = "lake"
|
||||
title = "lake"
|
||||
description = "a simple implementation of Scheme in C"
|
||||
url = "https://github.com/samsonjs/lake"
|
||||
|
||||
[[projects]]
|
||||
name = "AsyncMonitor"
|
||||
title = "AsyncMonitor"
|
||||
description = "easily monitor async sequences using Swift concurrency"
|
||||
url = "https://github.com/samsonjs/AsyncMonitor"
|
||||
|
||||
[[projects]]
|
||||
name = "NotificationSmuggler"
|
||||
title = "NotificationSmuggler"
|
||||
description = "embed strongly-typed values in notifications on Apple platforms"
|
||||
url = "https://github.com/samsonjs/NotificationSmuggler"
|
||||
|
||||
[[projects]]
|
||||
name = "strftime"
|
||||
title = "strftime"
|
||||
description = "strftime for JavaScript"
|
||||
url = "https://github.com/samsonjs/strftime"
|
||||
|
||||
[[projects]]
|
||||
name = "format"
|
||||
title = "format"
|
||||
description = "printf for JavaScript"
|
||||
url = "https://github.com/samsonjs/format"
|
||||
|
||||
[[projects]]
|
||||
name = "gitter"
|
||||
title = "gitter"
|
||||
description = "a GitHub client for Node (v3 API)"
|
||||
url = "https://github.com/samsonjs/gitter"
|
||||
|
||||
[[projects]]
|
||||
name = "cheat.el"
|
||||
title = "cheat.el"
|
||||
description = "cheat from emacs"
|
||||
url = "https://github.com/samsonjs/cheat.el"
|
||||
17
site.toml
Normal file
17
site.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
author = "Sami Samhuri"
|
||||
email = "sami@samhuri.net"
|
||||
title = "samhuri.net"
|
||||
description = "Sami Samhuri's blog about programming, mainly about iOS and Ruby and Rails these days."
|
||||
url = "https://samhuri.net"
|
||||
image_url = "/images/me.jpg"
|
||||
scripts = []
|
||||
styles = ["css/normalize.css", "css/style.css", "css/syntax.css"]
|
||||
|
||||
[projects_plugin]
|
||||
scripts = [
|
||||
"https://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js",
|
||||
"js/gitter.js",
|
||||
"js/store.js",
|
||||
"js/projects.js"
|
||||
]
|
||||
styles = []
|
||||
72
spec/config/loader_spec.rb
Normal file
72
spec/config/loader_spec.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
require 'spec_helper'
|
||||
require 'fileutils'
|
||||
require 'tmpdir'
|
||||
|
||||
RSpec.describe Pressa::Config::Loader do
|
||||
describe '#build_site' do
|
||||
it 'builds a site from site.toml and projects.toml' do
|
||||
with_temp_config do |dir|
|
||||
loader = described_class.new(source_path: dir)
|
||||
site = loader.build_site
|
||||
|
||||
expect(site.author).to eq('Sami Samhuri')
|
||||
expect(site.url).to eq('https://samhuri.net')
|
||||
expect(site.image_url).to eq('https://samhuri.net/images/me.jpg')
|
||||
expect(site.styles.map(&:href)).to eq(['css/style.css'])
|
||||
|
||||
projects_plugin = site.plugins.find { |plugin| plugin.is_a?(Pressa::Projects::Plugin) }
|
||||
expect(projects_plugin).not_to be_nil
|
||||
expect(projects_plugin.scripts.map(&:src)).to eq(['js/projects.js'])
|
||||
end
|
||||
end
|
||||
|
||||
it 'applies url_override and rewrites relative image_url with override host' do
|
||||
with_temp_config do |dir|
|
||||
loader = described_class.new(source_path: dir)
|
||||
site = loader.build_site(url_override: 'https://beta.samhuri.net')
|
||||
|
||||
expect(site.url).to eq('https://beta.samhuri.net')
|
||||
expect(site.image_url).to eq('https://beta.samhuri.net/images/me.jpg')
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises a validation error for missing required site keys' do
|
||||
Dir.mktmpdir do |dir|
|
||||
File.write(File.join(dir, 'site.toml'), "title = \"x\"\n")
|
||||
File.write(File.join(dir, 'projects.toml'), '')
|
||||
|
||||
loader = described_class.new(source_path: dir)
|
||||
expect { loader.build_site }.to raise_error(Pressa::Config::ValidationError, /Missing required site\.toml keys/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def with_temp_config
|
||||
Dir.mktmpdir do |dir|
|
||||
File.write(File.join(dir, 'site.toml'), <<~TOML)
|
||||
author = "Sami Samhuri"
|
||||
email = "sami@samhuri.net"
|
||||
title = "samhuri.net"
|
||||
description = "blog"
|
||||
url = "https://samhuri.net"
|
||||
image_url = "/images/me.jpg"
|
||||
scripts = []
|
||||
styles = ["css/style.css"]
|
||||
|
||||
[projects_plugin]
|
||||
scripts = ["js/projects.js"]
|
||||
styles = []
|
||||
TOML
|
||||
|
||||
File.write(File.join(dir, 'projects.toml'), <<~TOML)
|
||||
[[projects]]
|
||||
name = "demo"
|
||||
title = "demo"
|
||||
description = "demo project"
|
||||
url = "https://github.com/samsonjs/demo"
|
||||
TOML
|
||||
|
||||
yield dir
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue