Blog Posts

Helmets and Halberd Podcast

This morning, my RSS feed told me that Alex Schroeder started a podcast.

I have long admired Alex’s contributions. I did some ad-hoc editing for his Halberds and Helments RPG. The layout of Halberds and Helmets inspired me to go and learn about the Tufte style, which I have since adopted for my blog.

Below are excerpts from Episode 1 It’s about 14 minutes long in which Alex speaks to his goals and values for RPGs. I find myself in close alignment with Alex (though I do like the occassional system that requires mastery…looking at you Burning Wheel).

RPGS are about using your imagination to run the game; using rules to enforce consequences. It’s not just telling any sort of story, we are enforcing consequences.

Alex Schroeder

Agreed. The best RPGs have a good risk/reward tension.

What sort of rules do we want? What sort of stories do we want to tell? What sort of risks do we want to take?

Alex Schroeder

I’ve been circling around on this, and will be looking to have this conversation when my group wraps up Tomb of Annihilation.

Changing the rules is part of the game. We drop stuff we don’t like. We drop stuff we don’t remember. We add stuff we read online. We add stuff we decide at the table. And so the text evolves. It changes.

Alex Schroeder

Most everyone that runs and plays RPGs hacks on their system.

You can use the rules as written. You can start keeping notes. If your rules are short and your notes are long, then at some point you might as well write your own rules. It’s going to be a small book. But it’s going to be your book.

Alex Schroeder

Yes! Own your game.

I like the fear. I like my characters. I love them. But I also like to fear for them. This is what adventure is for me. When I as a player fear for my characters. That’s risk. That’s when I feel good. When I accomplish something. When I risked it. It didn’t just involve some talking. It involved facing the consequences.

Alex Schroeder

The most memorable moments of games are always when I know that I’ve risked my character, and that character through my ingenuity and luck of the dice roll, pulls off something memorable.

Keep record of how you want to run things.

Alex Schroeder

Yup.

Having few rules, means there is a lot of freedom. All the things that don’t require rules, we can handle it by just talking about it.

Alex Schroeder

I’m eyeing Whitehack or 5th Edition as my default base game. Both have a small rule set. Whitehack encourages more conversations.

Another thing I really like, is when the world doesn’t really move too fast. When it’s basically static…I don’t want to pressure the players to do this or that on a timer. If the world is going to end in 2 weeks. Then we really have to do just the thing. And who picked the thing? The referee picked the thing…forcing the plot on everybody else at the table. Therefore I say to my players, it’s going to be an open world…the game is going to grow where your in game actions go.

Alex Schroeder

Amen. The Tomb of Annihilation put a clock into play, forcing action. In doing so, much of what was written in the adventure falls out of the critical path.

I’ll try to provide information to players so they can change if these situations will be appropriate for them; because of the theme, danger, risk they are willing to take. That is where agency comes in. Where you can feel powerful about the decisions you can make. Because there was information. You acted upon it. And it changed the things that happened.

Alex Schroeder

Yes. I want to do more of that in my GM-ing.

Why simple mechanics? They have the benefit that they are quick to read…they benefit from remembering. Players know about 6 abilities, HP, AC, Damage, and Saving throws. This jump starts everything. It’s really great if you have Lady Blackbird in front of you, with all of the rules on your single character sheet. But if the rules are in a big book, that’s 200 pages long or more, then that really tires me and is harder to explain.

Alex Schroeder

To paraphrase, simple mechanics are about accessibility and minimal barrier to entry. They provide a minimum barrier for engaging with the fiction.

Epitath

Alex’s diary post pointed me to Judd Karlman’s Dreaming about Dragons podcast, another contributor to games that I’ve enjoyed following. Looks like I’ve got some listening to do.

Let’s Read “Stars without Number” - Starships


A part of my Let’s Read “Stars without Number” series. Go grab your free copy of SWN and join in.

Welcome back, from a bit of a hiatus. For this section we’ll dive into Starships. As you might expect, there is quite a bit going on.

The table of contents for this chapter is:

  • Building a Starship
  • Example Starships
  • Modifying and Tuning Starships
  • Space Travel
  • Sensors and Detection
  • Maintenance and Repair
  • Space Combat

Building a Starship

Some rather straight forward rules. Ships have a base hull Cost, Speed, Armor, HP, Crew (Min/Max), AC, Power, Mass, Hardpoints, and Hull Class.

  • Cost - this is the baseline cost for a ship with a basic drive.
  • Speed - the bonus to add to Piloting rolls for combat or maneuvering. Small ships higher speed.
  • Armor - the damage soaked by the hull
  • HP - the amount of damage the ship can take before it becomes inoperable (or explodes)
  • Crew - the minimum and maximum allowed crew
  • AC - like a character’s AC, denotes its difficulty to hit
  • Power - the amount of free power available for ship enhancements
  • Mass - the amount of free mass available for ship enhancements
  • Hardpoints - roughly the amount of weapons you can add to the ship
  • Hull Class - the category and relative size of the ship, limits what you may be able to add to the ship

Table 1: Base Starships
Hull Type Cost Speed Armor HP Crew AC Power Mass Hard Class
Strike Fighter 200K 5 5 8 1|1 16 5 2 2 Fighter
Free Merchant 500K 3 2 20 1|6 14 10 15 2 Frigate
Patrol Boat 2.5m 4 5 25 5|20 14 15 10 4 Frigate
Fleet Cruiser 10m 1 15 60 50|200 14 50 30 10 Frigate

From the base, you add fittings:

Table 2: Starship Fittings
Fitting Cost Power Mass Class Effect
Atmosphiric configuration 5k* 0 1# Fighter Can land: frigates and fighters only
Cargo space No cost 0 1 Fighter Pressurized cargo space
Drive-2 upgrade 10k* 1# 1# Fighter Upgrade a spike drive to drive-2 rating
Fuel scoops 5k* 2 1# Frigate Ship can scoop fuel from a gas giant or star
# - Costs x2 for Frigate, x3 for cruisers, x4 for capital ship
* - Costs x10 for Frigate, x25 for cruisers, x100 for capital ship

And some defenses.

Table 3: Ships Defense
Ship Defense Cost Power Mass Class Effect
Augmented Plating 25k* 0 1# Fighter +2 AC, -1 speed
Point Defense Lasers 10k* 3 2# Frigate +2 AC versus weapons that use ammo
# - Costs x2 for Frigate, x3 for cruisers, x4 for capital ship
* - Costs x10 for Frigate, x25 for cruisers, x100 for capital ship

And some weapons.

Table 4: Ships Weaponry
Ship Weapon Cost Dmg Power Mass Hard Class TL Qualities
Multifocal Laser 100k 1d4 5 1 1 Fighter 4 Armor Piercing 20
Flak Emitter Battery 500k 2d6 5 3 1 Frigate 4 Armor Piercing 10, Flak
Plasma Beam 700k 3d6 5 2 2 Frigate 4 Armor Piercing 10

By design, you will not have enough space to get what you want. As is, the resulting ship is a vanilla ship.

Example Starships

Stars without Numbers provides some example ships. Listing their stats, along with base cost and annual maintenance cost.

Table 5: Free Merchant
HP: 20 Power: 10|0 free
AC: 14 Mass: 15|0 free
Armor: 2 Crew: 1|6
Speed: 3 Hull Class: Frigate
Crew Skill: +1 NPC CP: 4*
Weapons: Multifocal Laser (+3 1d4, AP 20) Sandthrower (+3 2d4, Flak)
Defenses: None
Defenses: Spike Drive-1, 160 tons of cargo space Atmospheric Configuration Fuel Scoops, Fuel Bunker
Cost: 775K base price, 38,750 maintenance, 131,400 yearly crew cost for 3 crew
* - Command Points for the NPC crew to use in starship combat

Modifying and Tuning Starships

Tracking to the same system used in Equipment mods, engineers can add modifications to their ship. Key is the following: “[While the ship is in active use], if a ship mod is neglected for one week of active use, it breaks down.”

Think of the Millenium Falcon. “I made a lot of special modifications…” And those modifications require continuous attention from Chewbacca and Han Solo.

There are rules for Installing these special modifications. Or even redesigning a base hull (an expensive proposition). Each modification requires a base Fix skill, and a pre-tech component cost (e.g. the relics of an advanced time period long past).

Examples

Drill Velocity Upgrade (Fix-2): The ship’s spike drive is counted as one level better when determining spike drill transit speed and maximum drill range, up to a maximum of drive-6. Cost: 10% of hull, 1 component/hull class

Space Travel

For FTL, you drill through space. Your spike drive rating indicates how many hexes you can traverse in one drill (after which you’ll need to refuel). To prepare for a drill out, you need time and space. Yes you can rush it.

You use rutters (course records) to help plot. Commonly traveled paths are freely available, and after only a few days old.

Normally, the transit time takes 6 days per hex travelled, divided by the spike drill rating. You can trim the course, and treat the spike drill rating as one rank hirer for the purposes of calculating the transit time.

Navigators make a check.

Table 6: Spike Drills
Base difficulty for a spike drill 7
The course is totally uncharted +6
The rutter’s more than 5 years old +2
The rutter’s from 1 to 5 years old +1
The rutter’s less than a year old +0
The rutter’s less than a month old -2
The drill’s distance, per 2 full hexes +2
Trimming the course * +2
The drill activation was rushed +2

And what about failing the Spike Drill check?

If the roll fails, something has gone wrong on the drill, and a roll on the failed navigation results are necessary. These mishaps don’t normally result in the summary death of the crew, but they often force the navigator to make emergency course changes or drill exits that might leave the ship in a dangerously un- friendly region of space.

You roll on a table, to see what complications ensue.

There are also considerations for Intra-System Travel, higher spike drive ratings means faster intra-system travel.

Sensors and Detection

Sometimes you may want to be discreet about your travels, these rules are in play to provide a bit of cat and mouse. Also, if pirates are looking to ambush you, they’re going to use a comparable system.

Maintenance and Repair

Yup. Ships cost money. You’re going to always be scrounging for money to keep your ship space-worthy. Every six months [of mostly active use] you need to pay 5% of its base cost. The rules cover much of the antics you would expect based on Battlestar Galactica, Star Wars, and Firefly.

Space Combat

No miniatures on a battlemat, instead space combat uses a Ship’s Station-based system. Players fill the roll of Bridge, Captain, Comms, Engineering, and Gunnery; along with each being able to take General Actions.

The ship’s captain decides the order of departments and they each act acccordingly. As the mini-game presses on Ship Crises may emerge that require immediate attention.

Below is one example. A PC will need to take a Deal with a Crisis action and describe how they’re solving the problem; They’ll need to make a difficult check to resolve the issu.

Fuel Bleed: The ship’s fuel tanks have been holed or emergency vents have been force-triggered by battle damage. If not resolved by the end of the next round, the ship will jettison all fuel except the minimal amount needed for in-system operation

There is almost always something to do on the ship during a time of combat.

Conclusion

Packed into this chapter are the aspirations of every spacer. To get a ship, modify it, and engage in glorious snub fighter combat. And right alongside of it is the costs and challenges of owning said space ship.

Diving Further into Embarkation

This post follows-up on Playstorming Embarkation Tweaks in Adventures in Middle-earth. There are also a few conversations on Reddit regarding this series.


Adventures in Middle-earth from Cubicle 7

I keep coming back to the Embarkation system. I suppose it’s because it provides a subsystem that aims to emulate journeys without grinding through each hex. Also, within the Journey system, are clear transitions from “adventuring state” to “encounter state” as well as procedures for that transition. Several years ago, I wrote about transitions in RPGs. In essence, how does the game transition from one sub-system to another sub-system? Perhaps this is something to revisit.

A Bit More In-Depth

In the RAW system, Embarkation results drift towards a 12. Below is the 12 result:

12 (or more) From Auspicious Beginnings

The company sets out upon a path that will likely show them wonders long since forgotten or into dangers that most would quail at the thought of. But the auspices are good, and should the company be true, they will surely prevail.

As a result, add 2 to the rolls on the Journey Events Table. Additionally rolls made to determine the initial outcome of these encounters should be made with Advantage.

In the RAW system, as skill goes up, you begin skipping events on the Journey table; namely chance encounters with travellers and foraging opportunities. And push towards more encounters with powerful entities; 25% of your journey rolls will now result in 12s “Many Meetings? Fly You Fools!”; Below is an excerpt.

If the Embarkation roll was a 1, the encounter will automatically be with a servant of the Enemy. Conversely, if it was a 12 the company has encountered one of the great powers for Good. The outcome of such a meeting however, will depend on how the company approaches matters.

Without quoting the whole “Many Meetings? Fly You Fools!” the gist is as follows. As your Embarkation rolls trend upward, your chance encounters on the road drift from the following:

  • Powerful agents of the Enemy
  • Low-level strangers
  • Hunting and foraging opportunities
  • Powerful agents of Good; to

To:

  • Powerful agents of Good

Which may work from a campaign stand-point; As you gain in levels and the Shadow encroaches, you are likely to meet agents of Good moving throughout the land. But 25% of each travel encounter with agents of Good and advantage in that encounter seems a bit much.

In addition, the trend of your Embarkation rolls will be towards applying advantage to Journey rolls. Which raises some questions; Should your journey’s be getting ever easier? And if so, should you even roll for them? Should the implied “darkening” of the campaign world impact the Embarkation rolls? As time progresses and the Shadow strengthens (at least according to canon), might we consider modifications to the Embarkation roll?

All of this is speculation; I haven’t used the rules as written, but wanted to explore that sub-systems boundaries. And in a level-based system, such as 5E, with bound accuracy, we do need to examine the distribution of modifiers within that boundary.

I want the Journey subsystem to work. I’d love for the Journey process to help build out the world.

Postscript

I even wrote a script to generate results. It’s a lot of aggregate data that would require further complication and pivot tables to glean further insights. Please let me know if you do anything interesting with this.

Post-Postscript

The Embarkation and Journey tables makes a critical assumption that may not be in play for most sandbox games; There is a common map from which the players can plan their journeys. Not everything need be filled in on the map, but underneath that player facing layer is a layer that the Loremaster uses to adjudicate difficulty.

Playstorming Embarkation Tweaks in Adventures in Middle-earth


Adventures in Middle-earth from Cubicle 7

update: I posted this to Reddit, and there have been some great comments.

I’ve been on a deep dive into the Adventures of Middle-Earth. Originally attracted to the Journey rules as something to consider for Tomb of Annihilation, I’m wondering about the design goals of the Journey system.

Let’s review the Journey Rules

  • Players assign tasks and plan route.
  • Loremaster determines Peril Rating of the journey.
  • The Guide makes an Embarkation Roll: 1d12 modified by the Guide’s Survival proficiency bonus plus half their Wisdom bonus minus the Peril Rating.
  • The Loremaster either relays the result, or optionally hints at it.
  • Determine the number of Journey Events.
  • Events are played through, noting down the result for reference.
  • The Arrival roll (d8) is made, and results are applied.

Before I get started in breaking down the sub-system, I definitely want to say that I like the goal of this system: Make Journey’s meaningful relative to other aspects of play.

Digging In

I want to dig into “The Guide makes an Embarkation Roll”, and its odd formula:

  • 1d12 plus
  • Guide’s Survival Proficiency bonus (without Wisdom modifier) plus
  • 12 Wisdom bonus minus
  • Peril Rating (between 1 and 5)

With the Embarkation roll, you consult a table with a range of 1 or below upto 12 and above. The lower half is detremental to the party’s Journey rolls, the upper half is positive.

The most naive 1st level Player-hero (-1 Wis modifier) with no Survival proficiency travelling the most perilous route will have a -6 to their roll. Everything will go bad. If they take an easy journey, they’ll have a -2 to their roll: a 33% chance of a positive Embarkation.

The most skilled 1st level Player-hero (+3 Wis modifier) with Survival proficiency and Expertise (as an Open Cultural Trait) travelling the most perilous route will have a +0 to their roll (+32 +4 - 5). If they make an easy journey, they’ll have a +4 to their roll: an 84% chance of a positive Embarkation.

Once that skilled 1st level Player-hero bumps proficiency bonus (and Wisdom perhaps), they’ll have at least a +6 to their roll on the easiest terrain and at least a +2 for the most perilous journey.

Sifting Through

I’m not quite certain where to go from here. I suspect the design intention of the Embarkation, Journey, and Arrival mini-game is that journeys might grind you down. You’re not rolling for random encounters, nor really tracking rations, but instead playing the mini-game to see how travel goes.

Below is an initial pass at capturing what appears to be the intention of the Embarkation rules.

This procedure breaks the Embarkation roll into two separate rolls. First roll on the Guide’s DC for Starting off on a Journey table.

Table 1: Guide’s DC for Starting off on a Journey
Terrain Survival DC
Easy 7
Moderate 12
Hard 17
Severe 22
Daunting 27

Looking at the page 77 of the 5.1 SRD, I chose to split the difference between task difficulty.

On success the GM will roll 1d6+6 and consult the Adventures of Middle-earth Embarkation Table to determine the result. On a failure, the GM will roll 1d6 and consult the same table. I wonder if there should be a modifier for failing/succeeding by 5 or more; Perhaps a ±1 to the d6 roll? Or roll the d6 with disadvantage/advantage?

The end result is the Embarkation table is preserved, but the roll cleaves closer to the rules of the basic game.

Building Scripts for my Website

In foreground man in construction attire, holding rolled building plans jumping. In the background, a stucco home.
Building Joy. Released under CC0 Creative Commons; Free for commercial use; No attribution required.

Behind the scenes, I leverage a few different scripts that build then publish the site.

The build process See build script details below involves the following steps:

  • build:blogroll - fetch the latest entries for each blog in the blogroll
  • build:sort_frontmatter - I wanted the tags to always be alphabetical
  • build:hugo - this script leverages the hugo command to build the public pages
  • build:redirects - generate the redirects to preserve URLs
  • build:amplify - create the AMP-compliant HTML page
  • build:beautify - I like nice tabbed HTML source
  • build:minify - compress the underlying CSS
  • build:duplicate_feed - I have an old feed URL and a new feed URL I need to maintain; This creates that duplicate

Once built, the publish script See publish script details below pushes those changes to Github (which is my static site host).

The Code

Preamble

The pre-amble constants that are repeatedly used throughout


require 'toml-rb'
PROJECT_PATH = File.expand_path('../', __FILE__)
SITE_CONFIG = TomlRB.load_file(File.join(PROJECT_PATH, 'config.toml'))
PUBLIC_PATH = File.expand_path('../public', __FILE__)
ORGINAL_PROJECT_PATH = File.expand_path('../../takeonrules.github.io-source', __FILE__)

Structure and Method for Page

Function for loading a structural representation of a page/post. This is a common task for the website, so I’ve extracted a method and a structure to encode that behavior.


# Exposes a common data structure for interacting with a page/post
FileWithFrontmatterAndContent = Struct.new(:filename, :frontmatter, :body) do
  def content
    [Psych.dump(sorted_frontmatter).strip, '---', body].join("\n")
  end

  def tags
    frontmatter.fetch("tags", [])
  end

  def sorted_frontmatter
    returning_value = {}
    sorted_frontmatter = frontmatter.sort { |a,b| a[0] <=> b[0] }
    sorted_frontmatter.each do |key, value|
      if value.is_a?(Array)
        returning_value[key] = value.sort
      else
        returning_value[key] = value
      end
    end
    returning_value
  end
end
# Responsible for loading the given filename, separating frontmatter from content
# and returning a FileWithFrontmatterAndContent object
def load_file_with_frontmatter_and_content_from(filename)
  frontmatter_text = ''
  content = ''
  frontmatter = nil
  File.readlines(filename).each do |line|
    if line.strip == '---'
      if frontmatter.nil?
        frontmatter = true
        next
      elsif frontmatter == true
        frontmatter = false
      end
    elsif frontmatter
      frontmatter_text += line
    else
      content += line
    end
  end
  frontmatter = Psych.load(frontmatter_text)
  FileWithFrontmatterAndContent.new(filename, frontmatter, content)
end

Template and Method for Generating Redirects

Below is the HTML template and the code used to generate those redirect files.


# The template used when generating an HTML-page redirect.
REDIRECT_TEMPLATE = %(
<!DOCTYPE html>
<html>
  <head>
    <title>%{to}</title>
    <link rel="canonical" href="%{to}"/>
    <meta name="robots" content="noindex">
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <meta http-equiv="refresh" content="0; url=%{to}"/>
  </head>
  <body>
    <h1>Redirecting to %{to}</h1>
    <a href="%{to}">Click here if you are not redirected.</a>
  </body>
</html>
).strip

# Responsible for creating a redirect page based on the given paramters.
# The page will redirect to the given :to_slug, from the given :from_slug
def create_redirect_page_for(from_slug:, to_slug:, skip_existing_file: true)
  from_file_directory = File.join(PUBLIC_PATH, from_slug)
  from_filename = File.join(from_file_directory, 'index.html')
  if skip_existing_file && File.exist?(from_filename)
    $stdout.puts "\tSkipping #{from_slug}; Redirect already exists"
  else
    content = REDIRECT_TEMPLATE % { to: File.join(SITE_CONFIG.fetch("baseURL"), to_slug, '/') }
    FileUtils.mkdir_p(from_file_directory)
    $stdout.puts %(\tCreating redirect at "#{from_slug}")
    File.open(from_filename, 'w+') do |file|
      file.puts(content)
    end
  end
end

The Build Process

Below are the tasks that comprises the build process.


desc "Build the hugo site for production, target to ./public"
task build: [
  "build:blogroll",
  "build:sort_frontmatter",
  "build:hugo",
  "build:redirects",
  "build:amplify",
  "build:beautify",
  "build:minify",
  "build:duplicate_feed"
]

And the individual details of each of those build tasks.


namespace :build do
  desc 'Remove all, except .git, files in ./public'
  task :cleanDestinationDir do
    require 'fileutils'
    if !system("cd #{PUBLIC_PATH} && git checkout gh-pages && git reset --hard && git clean -df && git pull --rebase")
      $stderr.puts "Error cleaning destination directory, see above messages"
      exit!(1)
    end
    Dir.glob(File.join(PUBLIC_PATH, '*')).each do |filename|
      next if filename =~ /\.git$/
      FileUtils.rm_rf(filename)
    end
  end
  desc "Use hugo to build the ./public dir"
  task hugo: ["build:cleanDestinationDir"] do
    $stdout.puts "Buidling hugo site to ./public"
    if !system("cd #{PROJECT_PATH}; hugo")
      $stderr.puts "\tError building website"
      exit!(2)
    end
  end
  desc 'Using the ./data/redirects.yml, build redirects in ./public'
  task redirects: ["build:hugo"] do
    $stdout.puts "Creating Redirects…"
    require 'fileutils'
    require 'psych'
    redirects_file = File.join(PROJECT_PATH, 'data/redirects.yml')
    Psych.load_file(redirects_file).each do |redirect|
      create_redirect_page_for(
        from_slug: File.join('/', redirect.fetch('from'), '/'),
        to_slug: redirect.fetch('to'),
        skip_existing_file: redirect.fetch('skip_existing_file')
      )
    end
    $stdout.puts "\tDone Creating Redirects"
  end

  desc 'Working with the existing files, build AMP friendly versions in ./public'
  task amplify: ["build:hugo"] do
    require 'rest_client'
    require 'nokogiri'
    $stdout.puts "Amplifying the content…"
    # Need to clean-up stylesheet as it includes elements that are not AMP compatable
    stylesheet_content = ''
    skipping = false

    # Because there are style declarations that should not be included as they violate
    # AMP requirements
    tufte_filename = Dir.glob(File.join(PUBLIC_PATH, "css/tufte.*.css")).first
    File.readlines(tufte_filename).each do |line|
      next if line =~ /@charset/
      if line =~ /\A *\// # Remove comments
        if line.strip == "/* BEGIN SKIP-AMP */"
          skipping = true
        elsif line.strip == "/* END SKIP-AMP */"
          skipping = false
        end
        next
      else
        next if skipping
        stylesheet_content += line.strip + "\n"
      end
    end

    stylesheet_content = RestClient.post "https://cssminifier.com/raw", input: stylesheet_content

    # These scripts need to be injected into every page
    base_amp_scripts = []
    base_amp_scripts << %(<style amp-custom>#{stylesheet_content}</style>)
    base_amp_scripts << %(<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>)
    base_amp_scripts << %(<script async src="https://cdn.ampproject.org/v0.js"></script>)
    base_amp_scripts << %(<script async custom-element="amp-form" src="https://cdn.ampproject.org/v0/amp-form-0.1.js"></script>)
    base_amp_scripts << %(<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>)

    Dir.glob(File.join(PUBLIC_PATH, "**/*.html")).each do |filename|
      next if filename.start_with?(File.join(PUBLIC_PATH, 'assets'))
      next if filename.start_with?(File.join(PUBLIC_PATH, 'css'))
      next if filename.start_with?(File.join(PUBLIC_PATH, 'fonts'))
      next if filename.start_with?(File.join(PUBLIC_PATH, 'amp'))
      # Skipping tag as those are now in tags/
      next if filename.start_with?(File.join(PUBLIC_PATH, 'tag/'))

      amp_scripts = base_amp_scripts.clone

      # Checking blog posts
      if filename =~ /\/\d{4}\//
        amp_filename = filename.sub(/\/(\d{4})\//, '/amp/\1/')
      else
        # Checking pages
        amp_filename = filename.sub(PUBLIC_PATH, File.join(PUBLIC_PATH, 'amp'))
      end
      FileUtils.mkdir_p(File.dirname(amp_filename))
      content = File.read(filename)

      # Ensure that HTML is marked as AMP ready
      content.sub!(/^ *\<html /, '<html amp ')
      content.sub!(%(manifest="/cache.appcache"), '')
      content.sub!("hide-when-javascript-disabled", '')
      # Because details-tag is not valid in amp
      content.gsub!("<details open", "<span")
      # Likewise because details-tag and summary-tag are not valid in amp
      content.gsub!(/\<(\/?)(summary|details)/, '<\1span')

      doc = Nokogiri::HTML(content)
      # Convert img tag into an AMP compliant amp-img tag
      doc.css('img').each do |node|
        amp_img = doc.create_element('amp-img')
        src = node.get_attribute('src')
        width = node.get_attribute('data-width')
        height = node.get_attribute('data-height')
        amp_img.set_attribute('src', src)
        amp_img.set_attribute('width', width)
        amp_img.set_attribute('height', height)
        amp_img.set_attribute('layout', 'responsive')
        node.replace amp_img
      end

      added_iframe_script = false
      # Convert iframe tag into an AMP compliant amp-iframe tag
      doc.css('iframe').each do |node|
        amp_iframe = doc.create_element('amp-iframe')
        node.attributes.each do |key, value|
          next if key == 'marginheight'
          next if key == 'marginwidth'
          amp_iframe.set_attribute(key, value.to_s)
        end
        amp_iframe.set_attribute('sandbox', "allow-scripts allow-same-origin")
        amp_iframe.set_attribute('layout', "responsive")
        noscript = doc.create_element('noscript')
        noscript << node.clone
        node.parent << noscript
        node.replace amp_iframe
        next if added_iframe_script
        added_iframe_script = true
        amp_scripts << %(<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>)
      end

      doc.css('script').each do |node|
        # LD+JSON is valid for amp; All others are not
        next if node['type'] == 'application/ld+json'
        node.remove
      end
      doc.css('link[media]').each do |node|
        node.remove
      end

      # Because the license contains several problematic amp attributes,
      # I'm removing that license
      doc.css('.credits .license').each do |node|
        node.remove
      end
      content = doc.to_html

      content.sub!("</head>", amp_scripts.join("\n") + "\n</head>")
      amp_analytics = %(<amp-analytics type="gtag" data-credentials="include">\n<script type="application/json">\n{ "vars" : { "gtag_id": "#{SITE_CONFIG.fetch('googleAnalytics')}", "config" : { "#{SITE_CONFIG.fetch('googleAnalytics')}": { "groups": "default" } } } }\n</script>\n</amp-analytics>)
      content.sub!(/\<body([^\>]*)\>/, '<body\1>' + "\n#{amp_analytics}")

      File.open(amp_filename, 'w+') { |f| f.puts content }
    end
    $stdout.puts "\tDone Amplifying"
  end


  desc 'Beautify the HTML of the sites'
  task beautify: ["build:hugo", "build:redirects", "build:amplify"] do
    $stdout.puts "Beautfying the HTML…"
    # Redirects and resulting amp pages should be beautiful too
    require 'htmlbeautifier'
    require 'nokogiri'
    require 'json'
    Dir.glob(File.join(PROJECT_PATH, 'public', "**/*.html")).each do |filename|
      messy = File.read(filename)
      doc = Nokogiri::HTML(messy)
      doc.css('script').each do |node|
        next unless node['type'] == 'application/ld+json'
        begin
          json = JSON.dump(JSON.load(node.content))
          node.content = json
        rescue JSON::ParserError => e
          $stderr.puts "JSON parse error encountered in #{filename}"
          raise e
        end
      end
      messy = doc.to_html
      beautiful = HtmlBeautifier.beautify(messy, indent: "\t")
      File.open(filename, 'w+') do |f|
        f.puts beautiful
      end
    end
    $stdout.puts "\tDone Beautifying"
  end

  task duplicate_feed: ["build:hugo"] do
    # Because some sources have https://takeonrules.com/feed/ I need to resolve that behavior
    require 'fileutils'
    $stdout.puts "Duplicating and building externally published feeds"
    feed = File.join(PUBLIC_PATH, 'feed.xml')
    alternate_feed = File.join(PUBLIC_PATH, 'feed/index.xml')
    FileUtils.mkdir_p(File.join(PUBLIC_PATH, "feed"))
    FileUtils.cp(feed, alternate_feed)
  end

  task minify: ["build:hugo", "build:amplify"] do
    # Minify-ing CSS will remove some comments that are build switches for the amplify process
    # So amplify first
    require 'rest_client'
    $stdout.puts "Minifying CSS"
    # TODO as part of the amplify, I'd like to send along a minified CSS; For now, that just won't happen
    Dir.glob(File.join(PROJECT_PATH, 'public/css/**/*.css')).each do |filename|
      response = RestClient.post "https://cssminifier.com/raw", input: File.read(filename)
      File.open(filename, "w+") do |f|
        f.puts response
      end
    end
  end

  namespace :blogroll do
    desc "Fetch blogroll entries"
    task :fetch do
      if ENV["NO_BLOGROLL"]
        $stdout.puts "Skipping blog roll"
        next
      end
      $stdout.puts "Fetching blog roll entries"
      require 'rest_client'
      require 'nokogiri'
      require 'time'
      require 'psych'
      require 'feedjira'
      require 'uri'

      class BlogRollEntry
        attr_reader :site_url, :site_title, :item_pubDate, :item_title, :item_url, :author
        def initialize(xml:)
          feed = Feedjira::Feed.parse(xml)
          item = feed.entries.first
          uri = URI.parse(feed.url)
          @site_url = "#{uri.scheme}://#{uri.host}"
          @site_title = feed.title
          @item_pubDate = item.published.strftime('%Y-%m-%d %H:%M:%S %z')
          @item_url = item.url
          if item.title
            @item_title = item.title
          else
            @item_title = item.url.split("/").last.sub(/\.\w+$/, '').gsub(/\W+/, ' ')
          end
        end

        include Comparable
        def <=>(other)
          date_comparison = item_pubDate <=> other.item_pubDate
          return date_comparison unless date_comparison == 0
          site_title <=> other.site_title
        end

        def to_hash
          {
            "site_url" => site_url,
            "site_title" => site_title,
            "item_pubDate" => item_pubDate,
            "item_title" => item_title,
            "item_url" => item_url
          }
        end
      end

      entries = []
      blogroll = Psych.load_file(File.join(PROJECT_PATH, 'data/blogroll.yml'))
      blogroll.each do |feed_url|
        begin
          $stdout.puts "\tFetching #{feed_url}"
          response = RestClient.get(feed_url)
          entries << BlogRollEntry.new(xml: response.body)
        rescue RestClient::Exceptions::OpenTimeout
          $stdout.puts "Timeout for #{feed_url}, moving on"
        end
      end

      output = entries.sort.reverse.map(&:to_hash)

      File.open(File.join(PROJECT_PATH, 'data/blogroll_entries.yml'), 'w+') do |f|
        f.puts Psych.dump(output)
      end
    end

    desc "Commit blogroll entries"
    task :commit do
      if ENV["NO_BLOGROLL"]
        $stdout.puts "Skipping blog roll"
        next
      end
      message = "Updating blogroll entries\n\n```console\n$ bundle exec rake publish:blogroll\n```"
      $stdout.puts "Committing ./data/blogroll_entries.yml"
      system(%{cd #{PROJECT_PATH}; git add #{File.join(PROJECT_PATH, 'data/blogroll_entries.yml')}; git commit -m "#{message}"})
    end
  end

  desc "Sort frontmatter alphabetically"
  task :sort_frontmatter do
    require 'psych'
    $stdout.puts "Sorting front matter"
    Dir.glob(File.join(PROJECT_PATH, 'content/**', '*.md')).each do |filename|
      file_with_frontmatter_and_content = load_file_with_frontmatter_and_content_from(filename)
      File.open(filename, 'w+') do |f|
        f.puts file_with_frontmatter_and_content.content
      end
    end
  end

  task blogroll: ["build:blogroll:fetch", "build:blogroll:commit"]
  desc "Fetch blog roll entries"
end

The Publish Process

The task that is used to push the pages up to the server. Note that prior to running publish, I run the build process


desc "Publish changes to https://takeonrules.com"
task publish: :build do
  project_sha = `cd #{PROJECT_PATH} && git log --pretty=format:'%H' -1`.strip
  message = "Site updated at #{Time.now.utc}\n\nUsing SHA1 #{project_sha}\nfrom source repository\n\n```console\n$ bundle exec rake publish\n```"
  $stdout.puts "Committing ./public pages"
  system("cd #{PUBLIC_PATH} && git checkout gh-pages && git add . && git commit -am '#{message}' && git checkout master && git rebase gh-pages")
  $stdout.puts "Pushing ./public pages"
  system("cd #{PUBLIC_PATH} && git push origin gh-pages && git push origin master")
  $stdout.puts "Updating project's pointer for ./public submodule"
  system(%(cd #{PROJECT_PATH} && git add public && git commit -m "#{message}" && git push origin master))
end