makandra cards: A knowledge base on web development, RoR, and DevOps

What is makandra cards?

We are makandra, a team of 60 web developers, DevOps and UI/UX experts from Augsburg, Germany. We have firmly anchored the sharing of knowledge and continuous learning in our company culture. Our makandra cards are our internal best practices and tips for our daily work. They are read worldwide by developers looking for help and tips on web development with Ruby on Rails and DevOps.

15 years ago – in 2009 – we wrote our first card. Since then, over 6000 cards have been created, not o...

Bookmarklet to generate a commit message for an issue in Linear.app

Your commit messages should include the ID of the issue your code belongs to.
Our preferred syntax prefixes the issue title with its ID in brackets, e.g. [FOO-123] Avatars for users.
Here is how to generate that from an issue in Linear.

Add a new link to your browser's bookmarks bar with the following URL.

javascript:(() => {
  if (document.querySelector('[data-view-id="issue-view"]')) {
    const [id, ...words] = document.title.split(' ') ;
    prompt('Commit message:', `[${id}] ${words.join(' ')}`)
  } else {
    alert('Open issue...

Unpoly + Nested attributes in Rails: A short overview of different approaches

This card describes four variants, that add a more intuitive workflow when working with nested attributes in Rails + Unpoly:

  1. Without JS
  2. With HTML template and JS
  3. With HTML template and JS using dynamic Unpoly templates
  4. Adding Records via XHR and JS

Example

For the following examples we use a simple data model where a user has zero or more tasks.

Rails: Different flavors of concatting HTML safe strings in helpers

This card describes different flavors for concatting HTML safe strings in a helper method in Rails. You might want to use the tag helper instead of the content_tag helper (the tag helper knows all self closing tags).

Example

We want to generate HTML like this:

<h1>Navigation</h1>
<ul>
  <li>Left</li>
  <li>Right</li>
</ul>

Below you ca...

How to handle when an HTML <video> element cannot autoplay

HTML <video> elements can automatically start playing when the autoplay attribute is set on them. Except for when they can not, e.g. on pageload, or when the element enters the DOM without user interaction, or when the browser for some other reason decided to not start playing the video.

While there is no native "autoplay failed" event to listen to, you can wait for video data to be loaded and then check if the video actually started playing.

Example

<video autoplay>
  <source src="example.mp4" type="video/mp4" />
</video>
...

Using rack-mini-profiler (with Unpoly)

Debugging performance issues in your Rails app can be a tough challenge.

To get more detailed insights consider using the rack-mini-profiler gem.

Setup with Unpoly

Add the following gems:

group :development do
  gem 'memory_profiler'
  gem 'rack-mini-profiler'
  gem 'stackprof'
end

Unpoly will interfere with the rack-mini-profiler widget, but configuring the following works okayish:

// rack-mini-profiler + unpoly
if (process...

Caution: rem in @media query definitions ignore your font-size

Note

Using rem only ever makes sense when the root font size is dynamic, i.e. you leave control to the user. Either by a) respecting their user agent defaults, or by b) offering multiple root font sizes in your application.

By defining @media queries in rem, they will accommodate to the root font size of your page. At a larger root font, breakpoints will be at larger widths, scaling with the font. However, there is a catch in case b) mentioned in the note above.

Relative length units in media queries are based on the initial value,...

Problems with git submodules in Gitlab CI

If you are using git submodules in Gitlab CI, you might run into a "The project you were looking for could not be found or you don't have permission to view it."

Gitlab added a feature that new projects are no longer allowed to be cloned inside CI runs of other repositories by default. To fix this

  • Go into the project used as a submodule
  • Go to "Settings" -> "CI/CD" (if you don't see this section, enable it in "Settings" -> "General" -> "Visibility, project features, permissions")
  • Go to "Token Access"
  • Either disable "Limit access to ...

Rails: Testing file downloads with request specs

tl;dr

Prefer request specs over end-to-end tests (Capybara) to joyfully test file downloads!

Why?

Testing file downloads via Capybara is not easy and results in slow and fragile tests. We tried different approaches and the best one is just okay.

Tests for file downloads via Capybara ...

  • ... are slow,
  • ... are fragile (breaks CI, breaks if Selenium driver changes, ...),
  • ... need workarounds for your specia...

Ruby on Rails: Finding a memory leak

The author describes his little journey in hunting down a memory leak. Maybe his approach and tooling may one day help you in a similar situation.

Tools: rbtrace, ObjectSpace.trace_object_allocations_start, heapy, sheap

Accessibility: Making non-standard elements interactive

A common cause of non-accessible web pages are elements that were made interactive via JavaScript but cannot be focused or activated with anything but the mouse.

❌ Bad example

Let's take a look at a common example:

<form filter>
  <input filter--query name="query" type="text">
  <span filter--reset>Clear Search</span>
</form>

The HTML above is being activated with an Unpoly compiler like this:

up.compiler('[filter]', function(filterForm) {
  const resetButton = filterForm.querySelec...

Heads up: You should always use "current_window.resize_to" to resize the browser window in tests

I recently noticed a new kind of flaky tests on the slow free tier GitHub Action runners: Integration tests were running on smaller screen sizes than specified in the device metrics. The root cause was the use of Selenium's page.driver.resize_window_to methods, which by design does not block until the resizing process has settled:

We discussed this issue again recent...

Use <input type="number"> for numeric form fields

Any form fields where users enter numbers should be an <input type="number">.

Numeric inputs have several benefits over <input type="text">:

  • On mobile or tablet devices, number fields show a special virtual keyboard that shows mostly digit buttons.
  • Decimal values will be formatted using the user's language settings.
    For example, German users will see 1,23 for <input type="number" value="1.23">.
  • Values in the JavaScript API or when submitting forms to the server will always use a point as decimal separator (i.e. "1.23" eve...

Best practices: Writing a Rails script (and how to test it)

A Rails script lives in lib/scripts and is run with bin/rails runner lib/scripts/.... They are a simple tool to perform some one-time actions on your Rails application. A Rails script has a few advantages over pasting some prepared code into a Rails console:

  • Version control
  • Part of the repository, so you can build on previous scripts for a similar task
  • You can have tests (see below)

Although not part of the application, your script is code and should adhere to the common quality standards (e.g. no spaghetti code). However, a scri...

Opening a zipped coverage report with one click

Image

Tested on Ubunut 22.04

1. Opener script

  • Create a file ~/.local/bin/coverage_zip_opener with:
#!/bin/bash

tmp_folder="/tmp/coverage-report-opener"

if [ -z "$1" ]
then
 echo "Usage: coverage_zip_opener [filename]"
 exit -1
fi

if ! [[ "$1" =~ ^.*Pipeline.*Coverage.*\.zip$ || "$1" =~ ^.*merged_coverage_report.*\.zip$ ]]; then
 file-roller "$1"
 exit 0
fi

rm -Rf $tmp_folder

unzip -qq "$1" -d $tmp_folder
index_filename=$(find /tmp/coverage-report-opener -name "index.html" | ...

Element.animate() to animate single elements with given keyframes

Today I learned that you can animate HTML elements using the Web Animation API's method .animate(keyframes, options) (which seems to be Baseline for all browsers since 2022).

const fadeIn = [{ opacity: 0 }, { opacity: 1 }] // this is a keyframe example

const container = document.querySelector('.animate-me')

const animation = container.animate(fadeIn, 2000) // I am just using the animation duration as option here but it can also be given an object

// the animation object can be used for things like querying timings or stat...

Unpoly 3.7.1, 3.7.2 and 3.7.3 released

Version 3.7.0 broke some things in complex forms. Sorry for that. Concurrent user input is hard.

3.7.1

This change fixes two regressions for form field watchers, introduced by 3.7.0:

  • When a change is detected while waiting for an async callback, prevent the new callback from crashing with Cannot destructure property { disable } of null.
  • When a change is detected while waiting for an async callback, the full debounce delay of that new change is honored.

...

Capistrano: creating a database dump if migrating

In Capistrano 3, your Capfile requires 'capistrano/rails/migrations', which brings two Capistrano tasks: deploy:migrate and deploy:migrating. The former checks whether migrations should be performed. If so, the latter is invoked, which performs the actual migrations.

Knowing this, it is easy to dump the db only if migrations will run. First, enable conditional migrations:

# config/deploy.rb
set :conditionally_migrate, true # Only attempt migration if db/migrate changed

Then hook up the dump task to deploy:migrating:

RailsStateMachine 2.2.0 released

  • Added: State machine can now use the :prefix-option to avoid name collision if you define multiple state machines on the same model, and use state names more than once
    • Previously state_machine-definitions like this:
        class Form
        
          state_machine :first_wizard_stage do
            state :completed
          end
      
          state_machine :second_wizard_stage do
            state :completed
          end
          
        end
      
      would produce the following warning:
        rails_state_machine-2.1....
      

Javascript: Avoid using innerHTML for unsafe arguments

Make sure that you use the correct property when editing an HTML attribute. Using innerHTML with unsafe arguments makes your application vulnerable to XSS.

  • textContent: Sets the content of a Node (arguments are HTML-safe escaped)
  • innerHTML: Sets the HTML of an Element (arguments are not escaped and may not contain user content)

Hierarchy

This hierarchy gives you a better understanding, where the textContent and the innerHTML properties are defined. It also includes (just for completeness) the innerText property, whi...

Transfer records to restore database entries (with Marshal)

If you ever need to restore exact records from one database to another, Marshal might come in handy.

Marshal.dump is part of the ruby core and available in all ruby versions without the need to install anything. This serializes complete ruby objects including id, object_id and all internal state.

Marshal.load deserializes a string to an object. A deserialized object cannot be saved to database directly as the the dumped object was not marked dirty, thus rails does not see the need to save it, even if the object is not present in...

A reasonable default CSP for Rails projects

Every modern Rails app should have a Content Security Policy enabled.

Very compatible default

The following "default" is a minimal policy that should

  • "just work" for almost all applications
  • give you most of the benefits of a CSP

In your config/initializers/content_security_policy.rb, set

Rails.application.config.content_security_policy do |policy|
  policy.object_src :none
  policy.script_src :unsafe_eval, :strict_dynamic, :https # Browsers with support for "'strict-dynamic'" will ignore "https:"
  po...

How to: Upgrade CarrierWave to 3.x

While upgrading CarrierWave from version 0.11.x to 3.x, we encountered some very nasty fails. Below are the basic changes you need to perform and some behavior you may eventually run into when upgrading your application. This aims to save you some time understanding what happens under the hood to possibly discover problems faster as digging deeply into CarrierWave code is very fun...

Whitelists and blacklists

The following focuses on extension allowlisting, but it is the exact same thing for content type allowlisting with the `content_ty...

Zeitwerk: How to collapse folders in Rails

All direct child directories of app are automatically added to the eager- and autoload paths. They do NOT create a module for namespacing. This is intuitive, since there normally is no module Model, or module Controller. If you want to add a new base directory, there's no additional config needed.

Example

app
├── controllers
├── helpers
├── inputs # No config needed 
├── mailers
├── models
├── uploaders # No config needed
├── util # No config needed
└── workers # No config needed

Sometimes it's handy to group files wit...