diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 58b0017..ce8fe4e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: ruby: - - '3.2.0' + - '3.4.0' steps: - uses: actions/checkout@v3 - name: Set up Ruby diff --git a/CHANGELOG.md b/CHANGELOG.md index d32b747..ba69557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,72 +5,76 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -* `Added` for new features. -* `Changed` for changes in existing functionality. -* `Deprecated` for soon-to-be removed features. -* `Removed` for now removed features. -* `Fixed` for any bug fixes. +- `Added` for new features. +- `Changed` for changes in existing functionality. +- `Deprecated` for soon-to-be removed features. +- `Removed` for now removed features. +- `Fixed` for any bug fixes. + +## [1.4.0] - 2025-08-23 -## [Unreleased] +### Added + +- RSS ## [1.3.0] - 2024-01-05 ### Added -[#3](https://github.com/carlwiedemann/foresite/pull/3) version & watch commands (not sure how to test these best right now). +- version & watch commands (not sure how to test these best right now). ## [1.2.0] - 2023-02-11 ### Added -[#2](https://github.com/carlwiedemann/foresite/pull/2) Dynamic `` tag based on post title +- Dynamic `<title>` tag based on post title ### Fixed -* Typo in README. -* Link to blog post in README. +- Typo in README. +- Link to blog post in README. ## [1.1.3] - 2023-01-16 ### Fixed -* Use the top-level directory for the index.html file because you can't set subdirectories for github pages. +- Use the top-level directory for the index.html file because you can't set subdirectories for github pages. ## [1.1.2] - 2023-01-16 ### Fixed -* DRYing things up. +- DRYing things up. ## [1.1.1] - 2023-01-15 ### Fixed -* Small typo in gemspec. +- Small typo in gemspec. ## [1.1.0] - 2023-01-15 ### Added -* Can use templates for both post markdown and links list on index page. +- Can use templates for both post markdown and links list on index page. ### Fixed -* Reverse chronological order for posts on index page. -* Fleshed-out README and fixed typos. +- Reverse chronological order for posts on index page. +- Fleshed-out README and fixed typos. ## [1.0.1] - 2023-01-15 ### Changed -* Fleshed-out README and CHANGELOG files from default boilerplate. +- Fleshed-out README and CHANGELOG files from default boilerplate. ### Fixed -* Bug with trailing non-alphanumeric characters in post titles. +- Bug with trailing non-alphanumeric characters in post titles. ## [1.0.0] - 2023-01-14 ### Added -* Initial stable release, a global executable that provides the ability to generate HTML from markdown. +- Initial stable release, a global executable that provides the ability to generate HTML from markdown. diff --git a/README.md b/README.md index 73a8b7c..6bf44ee 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Create a project directory for your site and run `foresite init` from within it: Created erb/post.md.erb Created erb/wrapper.html.erb Created erb/_list.html.erb + Created erb/feed.xml.erb Three subdirectories are created, along with three [ERB](https://docs.ruby-lang.org/en/3.2/ERB.html) template files. @@ -52,6 +53,7 @@ Some facts: * `post.md.erb` is the default markdown file for every post. * `wrapper.html.erb` is a HTML wrapper template for every generated HTML file. * `_list.html.erb` is a HTML template partial for the list of posts on the `index.html` page. + * `feed.xml.erb` is a XML template for an RSS feed. ### 2. Write your first post @@ -69,8 +71,8 @@ A single markdown file is created in the `md` subdirectory. **This file is meant Some facts: -* The title is the first line formatted as H1 (mandatory). -* Current date in YYYY-MM-DD format is the first markdown paragraph (optional). +* The title is the first line formatted as H1. +* Current date in YYYY-MM-DD format is the first markdown paragraph. * Current date and title are "slugified" for filename. ### 3. Modify templates as desired @@ -81,6 +83,8 @@ Some facts: `_list.html.erb` is used to generate the `<ul>` list of posts on the `index.html` file. Modify to show posts in a different way. +`feed.xml.erb` is an RSS feed, it will require a `title` as well as a `base_url` for where you host your site. + ### 4. Generate HTML from markdown Run `foresite build` to create HTML in the `post` subdirectory and the `index.html` file: @@ -88,8 +92,9 @@ Run `foresite build` to create HTML in the `post` subdirectory and the `index.ht $ foresite build Created post/2023-01-15-welcome-to-my-site.html Created index.html + Created feed.xml -In this example, two HTML files are created. +In this example, two HTML files and an XML file are created. Some facts: @@ -97,7 +102,8 @@ Some facts: * A single `index.html` file shows a list of links to all posts in reverse-chronological order, prefixed with post date. * Post titles are parsed from the first H1 tag in each post markdown file. * Post dates are parsed from the post markdown filename. -* Re-running `foresite build` removes and recreates all HTML files in the `post` subdirectory as well as the `index.html` file. +* The `feed.xml` file reflects the list posts in RSS 2.0 format. +* Re-running `foresite build` removes and recreates all HTML files in the `post` subdirectory as well as the `index.html` file and `feed.xml` file. In this example, the `index.html` will contain: diff --git a/foresite.gemspec b/foresite.gemspec index eb1f27f..5317f04 100644 --- a/foresite.gemspec +++ b/foresite.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |spec| spec.summary = "An extremely minimal static site generator." spec.homepage = "https://github.com/carlwiedemann/foresite" spec.license = "MIT" - spec.required_ruby_version = ">= 2.7.0" + spec.required_ruby_version = ">= 3.3.0" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage @@ -28,19 +28,20 @@ Gem::Specification.new do |spec| "lib/foresite/version.rb", "lib/skeleton/_list.html.erb", "lib/skeleton/post.md.erb", - "lib/skeleton/wrapper.html.erb" + "lib/skeleton/wrapper.html.erb", + "lib/skeleton/feed.xml.erb" ] spec.bindir = "bin" spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "kramdown", "~> 2.4" - spec.add_dependency "thor", "~> 1.2" - spec.add_dependency "zeitwerk", "~> 2.6" + spec.add_dependency "kramdown", "~> 2.5" + spec.add_dependency "thor", "~> 1.4" + spec.add_dependency "zeitwerk", "~> 2.7" spec.add_dependency "filewatcher", "~> 2.1" - spec.add_development_dependency "rspec", "~> 3.2" - spec.add_development_dependency "standard", "~> 1.3" + spec.add_development_dependency "rspec", "~> 3.13" + spec.add_development_dependency "standard", "~> 1.50" spec.add_development_dependency "rake", "~> 13" end diff --git a/lib/foresite.rb b/lib/foresite.rb index 50c6e1a..7c78bd3 100644 --- a/lib/foresite.rb +++ b/lib/foresite.rb @@ -17,6 +17,7 @@ module Foresite FILENAME_POST_MD = "post.md.erb" FILENAME_WRAPPER_HTML = "wrapper.html.erb" FILENAME_LIST_HTML = "_list.html.erb" + FILENAME_FEED_XML = "feed.xml.erb" ENV_ROOT = "FORESITE_ROOT" @@ -68,6 +69,10 @@ def self.get_path_to_index_file File.join(get_path_to_root, "index.html") end + def self.get_path_to_feed_file + File.join(get_path_to_root, "feed.xml") + end + def self.relative_path(full_path) full_path.gsub(get_path_to_root, "").gsub(Regexp.new("^#{File::SEPARATOR}"), "") end @@ -98,6 +103,13 @@ def self.render_wrapped_index(links) }) end + def self.render_feed(items, date_build_822) + render_erb_file(FILENAME_FEED_XML, { + items: items.reverse, + date_build_822: date_build_822 + }) + end + def self.touch_directories [get_path_to_md, get_path_to_out, get_path_to_erb].map do |path| if Dir.exist?(path) @@ -110,7 +122,7 @@ def self.touch_directories end def self.copy_templates - [FILENAME_POST_MD, FILENAME_WRAPPER_HTML, FILENAME_LIST_HTML].map do |filename| + [FILENAME_POST_MD, FILENAME_WRAPPER_HTML, FILENAME_LIST_HTML, FILENAME_FEED_XML].map do |filename| full_file_path = File.join(get_path_to_erb, filename) if File.exist?(full_file_path) "#{relative_path(full_file_path)} already exists" diff --git a/lib/foresite/cli.rb b/lib/foresite/cli.rb index ae0b7d4..cff03ed 100644 --- a/lib/foresite/cli.rb +++ b/lib/foresite/cli.rb @@ -1,3 +1,5 @@ +require "date" + module Foresite ## # Cli class. @@ -88,6 +90,7 @@ def build # Wipe all output files. Dir.glob(File.join(Foresite.get_path_to_out, "*.html")).each { File.delete(_1) } File.delete(Foresite.get_path_to_index_file) if File.exist?(Foresite.get_path_to_index_file) + File.delete(Foresite.get_path_to_feed_file) if File.exist?(Foresite.get_path_to_feed_file) markdown_paths = Dir.glob(File.join(Foresite.get_path_to_md, "*.md")) @@ -106,11 +109,14 @@ def build File.write(html_path, Foresite.render_wrapped(title, markdown_content)) $stdout.puts("Created #{Foresite.relative_path(html_path)}") - # Extract date if it exists. + # Extract date. match_data = /\d{4}-\d{2}-\d{2}/.match(filename_markdown) + date_ymd = match_data[0] + date_822 = DateTime.strptime(date_ymd, "%F").strftime("%a, %d %b %Y %H:%M:%S %z") { - date_ymd: match_data.nil? ? "" : match_data[0], + date_ymd: date_ymd, + date_822: date_822, href: Foresite.relative_path(html_path), title: title } @@ -119,8 +125,11 @@ def build # Generate index file. index_html_path = Foresite.get_path_to_index_file File.write(index_html_path, Foresite.render_wrapped_index(links)) - $stdout.puts("Created #{Foresite.relative_path(index_html_path)}") + + feed_xml_path = Foresite.get_path_to_feed_file + File.write(feed_xml_path, Foresite.render_feed(links, links.last[:date_822])) + $stdout.puts("Created #{Foresite.relative_path(feed_xml_path)}") end desc "watch", "Watches markdown and templates files and runs `build` when they change" diff --git a/lib/foresite/renderer.rb b/lib/foresite/renderer.rb index 4f06509..16f1a4f 100644 --- a/lib/foresite/renderer.rb +++ b/lib/foresite/renderer.rb @@ -12,6 +12,7 @@ class Renderer # @param [Hash] vars Variables for template. def initialize(path, vars) @path = path + @vars_original = @vars vars.each do |k, v| if k.is_a?(Symbol) instance_variable_set(:"@#{k}", v) diff --git a/lib/foresite/version.rb b/lib/foresite/version.rb index 6de3f37..1591f6f 100644 --- a/lib/foresite/version.rb +++ b/lib/foresite/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Foresite - VERSION = "1.3.0" + VERSION = "1.4.0" end diff --git a/lib/skeleton/feed.xml.erb b/lib/skeleton/feed.xml.erb new file mode 100644 index 0000000..28830b6 --- /dev/null +++ b/lib/skeleton/feed.xml.erb @@ -0,0 +1,23 @@ +<% + title = 'Another Foresite Blog' + base_url = 'https://example.com' +-%> +<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> + <channel> + <title><%= title %> + <%= base_url %>/feed.xml + en-us + <%= @date_build_822 %> + <%= @date_build_822 %> + Foresite + + <% @items.each do |item| -%> + + <%= item[:title] %> + <%= "#{base_url}/#{item[:href]}" %> + <%= "#{base_url}/#{item[:href]}" %> + <%= item[:date_822] %> + + <%- end %> + + diff --git a/lib/skeleton/wrapper.html.erb b/lib/skeleton/wrapper.html.erb index b76ba6f..c81e939 100644 --- a/lib/skeleton/wrapper.html.erb +++ b/lib/skeleton/wrapper.html.erb @@ -6,6 +6,7 @@ <%= @title ? "#{@title} - #{index_title}" : index_title %> + diff --git a/spec/foresite/cli_spec.rb b/spec/foresite/cli_spec.rb index 5f10325..9984bba 100644 --- a/spec/foresite/cli_spec.rb +++ b/spec/foresite/cli_spec.rb @@ -33,7 +33,8 @@ "Created erb/", "Created erb/post.md.erb", "Created erb/wrapper.html.erb", - "Created erb/_list.html.erb" + "Created erb/_list.html.erb", + "Created erb/feed.xml.erb" ]) expect { Foresite::Cli.new.invoke(:init) }.to output(expected_stdout).to_stdout @@ -60,7 +61,8 @@ "erb/ already exists", "erb/post.md.erb already exists", "erb/wrapper.html.erb already exists", - "erb/_list.html.erb already exists" + "erb/_list.html.erb already exists", + "erb/feed.xml.erb already exists" ]) # Invoke first time. @@ -154,7 +156,9 @@ Foresite::Cli.new.invoke(:touch, ["Jackdaws Love my Big Sphinx of Quartz!"]) Foresite::Cli.new.invoke(:touch, ["When Zombies Arrive, Quickly Fax Judge Pat"]) - ymd = Time.now.strftime("%F") + now = Time.now + ymd = now.strftime("%F") + rfc822 = now.strftime("%a, %d %b %Y 00:00:00 +0000") path_to_first = "#{tmpdir}/md/#{ymd}-jackdaws-love-my-big-sphinx-of-quartz.md" # Simulate the first file being written previously. @@ -164,11 +168,13 @@ expected_path_first = "#{tmpdir}/post/2022-12-25-jackdaws-love-my-big-sphinx-of-quartz.html" expected_path_second = "#{tmpdir}/post/#{ymd}-when-zombies-arrive-quickly-fax-judge-pat.html" expected_path_index = "#{tmpdir}/index.html" + expected_path_feed = "#{tmpdir}/feed.xml" expected_stdout = ForesiteRSpec.cli_lines([ "Created post/2022-12-25-jackdaws-love-my-big-sphinx-of-quartz.html", "Created post/#{ymd}-when-zombies-arrive-quickly-fax-judge-pat.html", - "Created index.html" + "Created index.html", + "Created feed.xml" ]) # Run build @@ -198,10 +204,26 @@ EOF + expected_content_feed = <<~EOF + + When Zombies Arrive, Quickly Fax Judge Pat + https://example.com/post/#{ymd}-when-zombies-arrive-quickly-fax-judge-pat.html + https://example.com/post/#{ymd}-when-zombies-arrive-quickly-fax-judge-pat.html + #{rfc822} + + + Jackdaws Love my Big Sphinx of Quartz! + https://example.com/post/2022-12-25-jackdaws-love-my-big-sphinx-of-quartz.html + https://example.com/post/2022-12-25-jackdaws-love-my-big-sphinx-of-quartz.html + Sun, 25 Dec 2022 00:00:00 +0000 + + EOF + # HTML file contents should contain generated markdown. expect(File.read(expected_path_first)).to include(expected_content_first) expect(File.read(expected_path_second)).to include(expected_content_second) expect(File.read(expected_path_index)).to include(expected_content_index) + expect(File.read(expected_path_feed)).to include(expected_content_feed) # They should also use the top-level HTML template, we can just use a dummy string to confirm. expected_title_index = "Another Foresite Blog" @@ -210,6 +232,7 @@ expect(File.read(expected_path_first)).to include(expected_title_first) expect(File.read(expected_path_second)).to include(expected_title_second) expect(File.read(expected_path_index)).to include(expected_title_index) + expect(File.read(expected_path_feed)).to include(expected_title_index) end end