Shuhei Kagawa

2019 in Review

Dec 31, 2019

Aegina island in April

Travels

I visited six new countries and enjoyed each of them.

  • January: Japan
  • March: Budapest, Hungary
  • April: Athens and Aegina, Greece
  • April: Wrocław, Poland
  • June: Prague, Czech Republic
  • August: Brussels and Bruges, Belgium
  • September: Munich, Germany
  • October: Dubrovnik, Croatia
  • November: Japan

There are many more places to visit in Europe. I'll keep traveling in 2020.

Bike

I bought an entry-level road bike at Decathlon in June. I used it for commuting and made day trips around Berlin. Berlin is surrounded by amazing fields. Blankenfelde is my favorite so far. I can't wait for the next Spring.

Budgeting

I started using YNAB at the end of the last year because a few of my friends were using it. I have used it throughout the year, and it's the first budgeting system that worked well for me. It helps us traveling regularly while saving money.

Books

I bought 55 physical books and 5 ebooks. That's three times more than the last year, probably because of a dedicated budget for buying books.

I finished reading 14 books and stranded somewhere in the middle of many books. Quiet made me more introverted and now I spend more time at home. Bad Blood and Educated blew my mind.

Drink

While it's hard to name the best food of the year, the best beer of 2019 was a porter at Kontynuacja in Wrocław, Poland. The city had high-quality craft beers, and everything that I drank was amazing.

I had a chance to meet one of my most favorite brewers, Fuerst Wiacek, at a tap takeover event at Biererei Bar. I bought a T-shirt.

The trip to Brussels was epic as well because I visited a traditional lambic brewery where they still brew beer with yeasts in the air of the building! After tasting sour lambic, I bought a T-shirt there too.

In spite of the encounters with good beers, I don't drink as much as I used to anymore. When I started commuting by bike, I didn't want to get drunk and ride a bike. Even after I stopped commuting with my bike in the winter, the momentum kept going. I drank almost every day in Japan as an exception, but I almost quit drinking again after coming back to Berlin.

On the other hand, I started drinking bubble tea regularly. There is a Comebuy shop near my office. I tried several bubble tea shops in Tokyo and found that only a few were better than Comebuy—I liked Yi Fang most.

Work

I am still working on the same team in Zalando. I looked for new opportunities inside and outside the company but decided to stay a bit more.

My team and I started on-call duties. Before that, another on-call team was taking care of my team's applications, and we tried to make sure that they didn't get called. Our focus on reliability hasn't changed much, but being on-call triggered new learnings. Writing post-mortem documents is my new favorite activity.

I started interviewing regularly, mostly on coding. I'm still not used to it, and there's more to learn.

Side Projects

In the first half of the year, I wrote a small metrics utility for work and a toy interpreter and compiler in Rust for fun. In the last half, I focused more on learning classic algorithms than side projects.

Conferences & Meetups

I attended two conferences, JSConf EU and JSConf JP, and had a chance to speak at a meetup.

2020

Looking back, 2019 was fun. I wish 2020 would be a happy year for all of you!

Winter Terminal (mostly Vim) Cleaning

Dec 31, 2019 - Vim

In December, I spent some time cleaning up my terminal setup. Dust had piled up in a year, and my terminal was getting slower. It was time to dust off.

Here are highlights of the changes.

Faster Text Rendering

I noticed a non-negligible lag when I was editing JavaScript/TypeScript in Neovim. At first, I thought some Vim plugins caused it. But it was not true. Not only editing was slow, but also scrolling was slow. Text rendering itself was the problem.

I opened files of different types in Vim's vertical split and less in tmux's vertical split. And I scrolled down and (subjectively) evaluated the smoothness of scrolling.

It turned out that Vim was not the problem. With vertical splits of tmux, even less command was slow to scroll. Regardless of Vim or tmux, text rendering in vertical splits was slow on iTerm2. In retrospect, it makes sense because iTerm2 doesn't know about vertical split by Vim or tmux and can't limit rendering updates to the changed pane. iTerm2's tmux integration may have helped, but I didn't try that.

I tried Alacritty, and it was much faster! I had been using Alacritty before but switched back to iTerm2 for font ligatures. Now I didn't care much about font ligatures—ligatures look pretty, but glyphs for != and !== confused me in JavaScript. So I switched to Alacritty again.

Also, I stopped using flatlandia color scheme in Vim, and it improved the rendering speed a bit. I didn't dig into why, though.

fzf.vim

fzf.vim was a life changer. It provides a blazing fast incremental search for almost anything. I use it for file names (instead of ctrlp.vim), commit history and grep. Especially, incremental grep with a preview is amazing.

More Vim Cleaning

  • Started using ale as a Language Server Protocol client. I was using ale for linting and fixing, and LanguageClient-neovim for LSP features. LanguageClient-neovim also shows a quickfix window when a file contains syntax errors and was conflicting with ale. I learned that ale supported LSP as well and made it handle LSP too.

    • Update on Jan 3, 2020: I started using coc.nvim instead of ale and deoplete.nvim for autocomplete, linting, fixing and LSP features. It makes Vim an IDE. Simply incredible.
  • Configured Vim to open :help in a vertical split. :help is a valuable resource when configuring Vim. The problem for me was that Vim opens help in a horizontal split by default. Opening help in a vertical split makes it much easier to read.

    autocmd FileType help wincmd H
  • Sorted out JavaScript/JSX/TypeScript syntax highlighting. Vim sets javascriptreact to .jsx and typescriptreact to .tsx by default. But those file types don't work well with the plugin ecosystem because plugins for javascript/typescript file types don't work with javascriptreact/typescriptreact and popular JSX/TSX plugins use javascript.jsx and typescript.tsx.

    autocmd BufRead,BufNewFile *.jsx set filetype=javascript.jsx
    autocmd BufRead,BufNewFile *.tsx set filetype=typescript.tsx
  • Stopped unnecessarily lazy-loading Vim plugins with dein.vim. I had configured file-type-specific plugins as lazy plugins of dein.vim without understanding much. The truth was that lazy plugins are meaningful only for plugins with plugin directory. Most of the file-type-specific plugins don't have plugin directory and are lazily loaded by default with ftdetect and ftplugin. :echo dein#check_lazy_plugins() shows those plugins that are ill-configured. I finally learned what those plugin directories do after using Vim for several years...

  • Reviewed key mappings and removed waiting time by avoiding mappings that prefixed other mappings. For example, I had mappings of ,g and ,gr. ,g was slow because Vim had to wait for a while to determine it was ,g or ,gr.

  • Tried Vim 8 but switched back to Neovim. Vim 8 worked well, but tiny details looked smoother in Neovim. For example, when syntax highlighting hangs up, Vim 8 hangs up while Neovim disables syntax highlighting and goes on.

  • Started documentation of my setup. I keep forgetting key mappings, useful plugins that I occasionally use, how things are set up, etc.

Generating Twitter Card Images from Blog Post Titles

Oct 13, 2019 - JavaScript

Twitter shows links to some websites as nice cards with images, but not for all websites. I realized that Twitter didn't show the card for my blog. Why? It turned out that they were called Twitter Cards, and Twitter showed them for websites that provided specific metadata. Is it common sense? Maybe, but I didn't know.

Twitter Cards give websites an ability to add an image, a video, etc. when they are shared on Twitter. A Twitter Card makes a tweet (physically) 3x more visible on the timeline. This post explains how I generated images from post titles using node-canvas, inspired by Hatena Blog.

Twitter Card preview

Meta tags

Twitter's bots look for <meta> tags in your page. If your page has a certain meta tags, it shows a Twitter Cards for links to the page. Check the documentation for more details. The <meta> tags look like these:

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@your_twitter_account" />

<meta name="twitter:title" content="My Blog Post" />
<meta name="twitter:description" content="This is a blog post." />
<meta name="twitter:image" content="https://test.com/images/foo.png" />

Uh, they look a bit too platform-specific. twitter:card and twitter:site are specific to Twitter, but what about twitter:title, twitter:description and twitter:image? Twitter's bots also pick up Open Graph metadata tags, which are also used by other platforms like Facebook. So, we can use the og: tags instead of twitter: tags. Be careful that the attribute name of Open Graph metadata is property instead of name!

<meta property="og:title" content="My Blog Post" />
<meta property="og:description" content="This is a blog post." />
<meta property="og:image" content="https://test.com/images/foo.png" />

Homemade static site generator

My blog is built with gulp and some custom plugins and deployed to GitHub Pages. I started the blog with Octopress several years ago and rewrote it with gulp when I was fascinated with gulp and JavaScript build tools. I once added React as a template engine and removed it later. Because of the history, its directory structure stays similar to the original one of Octopress. I write markdown files like source/_posts/2019-10-13-foo.md and the build system generates HTMLs like /blog/2019/10/13/foo/index.html.

To add Open Graph meta tags, I wrote a gulp plugin. Each gulp plugin is a transform stream that consumes and produces vinyl file objects. First, I made the plugin to extract image URLs from HTML and added necessary meta tags to the HTML template for <head> tag. Now, posts with at least one image got Twitter Cards.

Image generation and text wrapping

Most of my posts didn't have any images, while Twitter Cards don't look great without images. But I'm too lazy to create an image for each blog post manually.

I found that Hatena Blog, a blogging platform in Japan, was generating images from blog post titles and descriptions. It's a neat idea to promote blog posts without manual effort of blog authors. Can I replicate the image generation?

I found that many image-generation npm packages were using node-canvas. It provides the canvas API for Node.js and supports export options, including PNG. I decided to try that.

The canvas API was easy to use for me, but it doesn't provide text wrapping. I needed to come up with a way to break texts into lines. As a Q&A on Stackoverflow suggested, I used ctx.measureText(text) to measure the width of the text and remove words until the subtext fits the given width. And do the same for the remaining text.

The first line of this text wrapping algorithm is visualized as follows (it actually happens on the same line, but showing each try in its line for illustration):

Wrapping text

There were two edge cases to be covered. The first case is that a long word doesn't fit into the given width. The other case is that the text is split into too many lines, and they overflow the given height. I covered them by decreasing the font size until the entire text fits into the given rectangle.

The algorithm for the first edge case is visualized as follows (it tries smaller fonts until the word fits into the width):

Try smaller font sizes

I eventually came up with JavaScript code like this (the full code is on GitHub):

function fitTextIntoRectangle({ ctx, text, maxFontSize, rect }) {
  // Reduce font size until the title fits into the image.
  for (let fontSize = maxFontSize; fontSize > 0; fontSize -= 1) {
    ctx.font = getTitleFont(fontSize);
    let words = text.split(" ");
    let { y } = rect;
    const lines = [];
    while (words.length > 0) {
      let i;
      let size;
      let subtext;
      // Remove words until the rest fit into the width.
      for (i = words.length; i >= 0; i -= 1) {
        subtext = words.slice(0, i).join(" ");
        size = ctx.measureText(subtext);

        if (size.width <= rect.width) {
          break;
        }
      }

      if (i <= 0) {
        // A word doesn't fit into a line. Try a smaller font size.
        break;
      }

      lines.push({
        text: subtext,
        x: rect.x,
        y: y + size.emHeightAscent
      });

      words = words.slice(i);
      y += size.emHeightAscent + size.emHeightDescent;
    }

    const space = rect.y + rect.height - y;
    if (words.length === 0 && space >= 0) {
      // The title fits into the image with the font size.
      // Vertically centering the text in the given rectangle.
      const centeredLines = lines.map(line => {
        return {
          ...line,
          y: line.y + space / 2
        };
      });
      return {
        fontSize,
        lines: centeredLines
      };
    }
  }

  throw new Error(
    `Text layout failed: The given text '${text}' did not fit into the given rectangle ${JSON.stringify(
      rect
    )} even with the smallest font size (1)`
  );
}

Font

My website is using IBM Plex Sans via Google Fonts. I wanted to use the same font in the images. Fortunately, node-canvas provides an API to load fonts, and the font is available also on npm.

yarn add -D @ibm/plex
const { registerFont } = require("canvas");

registerFont(
  "./node_modules/@ibm/plex/IBM-Plex-Sans/fonts/complete/otf/IBMPlexSans-Bold.otf",
  {
    family: "IBM Plex Sans",
    weight: "bold"
  }
);

// ...

ctx.font = "bold 30px 'IBM Plex Sans'";

Done!

So, the feature is done. It looked trivial at first glance, but the text wrapping algorithm was fun to write. Now I got to write more blog posts to use this feature!