Hello.

I am Paul Kinlan.

A Developer Advocate for Chrome and the Open Web at Google.

Thinking about Developer Satisfaction and Web Developers

Paul Kinlan

It would be an understatement to say that MDN’s Developer Needs Assessment has helped the Chrome Web Platform team prioritise their work for 2020, in fact it’s been central to DevRel and the Web Platform teams objective setting (more on that soon). Over the last year or so we’ve been fortunate to collaborate with the MDN team via the PAB (Product Advisory Board), and during that time we’ve been thinking about how we measure the satisfaction that developers have with the platform, and even if it’s important.

Read More

Paul Kinlan

Trying to make the web and developers better.

RSS Github Medium

Hiring: Chrome Privacy Sandbox Developer Advocate🔗

Paul Kinlan

It’s looking like 2020 will be a big year for Privacy across the web and our team (Chrome) is no exception.

Chrome has a rather large number of projects that are coming in the following years that will continue to improve the privacy of all users on the web and we need the help of an awesome Developer Advocate to ensure that the entire cross-browser privacy story is heard, understood and implemented across the web.

The Developer Advocate role will help to accelerate the adoption of security and privacy related primitives from all browsers (think about all the great work browsers like Firefox, Safari, Brave etc are doing) across the web ecosystem and to make sure that our engineering and product teams are prioritising the needs of users and developers. It’s not going to be easy, because a lot of these changes impact the way developers build sites today; for example, the Same-Site change that is landing in Chrome imminently requires developers of widgets and anything that is hosted on a 3rd party origin meant to be used in a 1st party context, to declare that the correct SameSite attribute, lest they be automatically set to SameSite=Lax, which will restrict their usage slightly.

There’s going to be a lot of work to do, so being able to work with companies, frameworks and libraries in the ecosystem is going to be a key part of this role.

If you’re interested, my email is paulkinlan@google.com - or you can apply on the Job posting directly.

Correct image orientation for images - Chrome 81

Paul Kinlan

Looks like one of the oldest bugs in Chromium has been fixed. Chrome used to display images in landscape orientation even if they were taken in portrait on a phone. Now it appears that in Chrome 81 we will respect the image orientation from the files EXIF data and display the image in that orientation by default unless you override it with a CSS attribute image-orientation: none. Fun times.

Read More

Light fork of SimpleImage for Editor.js🔗

Paul Kinlan

I love Editor.js. It’s a nice simple block editor that I use to write these posts. It has a host of good plugins that enable you to extend the capabilities of the editor, such as the SimpleImage tool that allows you to add images to the editor without having to upload them.

It’s the SimpleImage that I briefly want to talk about. It’s a good tool, but it has two problems, 1) I can only drag images on to the editor, I can’t “add” an image; 2) It uses base64 data URL’s to host the image, this is a waste of memory and it should be using blob URLs.

I wrote a simple fork that addresses these two pain points. The first is that it uses less memory because it uses blob URLs. The second is that now you can add images in when adding in a new Block to the editor.

In fact, the following images are added using this new way.

Add Image
Choose file
Image added

Airhorner with added Web USB🔗

Paul Kinlan

This new year Andre Bandarra left me a little surprise on my desk: A physical airhorner built with Web USB!

Check it out, well actually it will be hard, Andre created a small sketch for an Arduino Uno that connects over USB that is not yet available, however the code on the site is rather neat and not too complex if you are experienced with any form of USB programming.

Andre’s code connects to the device and waits for the user to approve, configures the connection, and then continuously reads from the device looking for the string ‘ON’ (which is a flag that is set when the button is pressed).

const HardwareButton = function(airhorn) {
  this.airhorn = airhorn;
  this.decoder = new TextDecoder();
  this.connected = false;
  const self = this;
  this._loopRead = async function() {
    if (!this.device) {
      console.log('no device');
      return;
    }

    try {
      const result = await this.device.transferIn(2, 64);
      const command = this.decoder.decode(result.data);
      if (command.trim() === 'ON') {
        airhorn.start({loop: true});
      } else {
        airhorn.stop();
      }
      self._loopRead();
    } catch (e) {
      console.log('Error reading data', e);
    }
  };

  this.connect = async function() {
    try {
      const device = await navigator.usb.requestDevice({
        filters: [{'vendorId': 0x2341, 'productId': 0x8057}]
      });
      this.device = device;
      await device.open();
      await device.selectConfiguration(1);
      await device.claimInterface(0);
      await device.selectAlternateInterface(0, 0);
      await device.controlTransferOut({
        'requestType': 'class',
        'recipient': 'interface',
        'request': 0x22,
        'value': 0x01,
        'index': 0x00,
      });
      self._loopRead();
    } catch (e) {
      console.log('Failed to Connect: ', e);
    }
  };

  this.disconnect = async function() {
    if (!this.device) {
      return;
    }

    await this.device.controlTransferOut({
      'requestType': 'class',
      'recipient': 'interface',
      'request': 0x22,
      'value': 0x00,
      'index': 0x00,
    });
    await this.device.close();
    this.device = null;
  };

  this.init = function() {
    const buttonDiv = document.querySelector('#connect');
    const button = buttonDiv.querySelector('button');
    button.addEventListener('click', this.connect.bind(this));
    button.addEventListener('touchend', this.connect.bind(this));
    if (navigator.usb) {
      buttonDiv.classList.add('available');
    }
  };

  this.init();
};

If you are interested in what the Arduino side of things looks like, Andre will release the code soon, but it’s directly inspired by the WebUSB examples for Arduino.

Matsushima, Miyagi

Paul Kinlan

Before we visited Yamadera we spent the day in Matsushima in Miyagi prefecture. It was an amazingly beautiful seaside town about an hour away from Sendai on the train, that is famous for it’s fresh Oysters, many islands and a number of rather fabulous red bridges. Matsushima got hit by the tsunami in 2011 (although the town says that the islets slowed down the tsunami a lot) and all around the town there area a lot of sobering reminders to how high the water got and the damage it caused.

Read More

Yamadera, Yamagata

Paul Kinlan

Early in December we took a short day-trip to Yamadera Temple in Yamagata prefecture.  The temple area is said to be about 1000 years old, with the oldest remaining building being about 400 years old. The temple is built on to the side of a mountain that you have to walk up (it’s not too strenuous to reach the top), but it presents amazing views across the valley from the top.

Read More

Modern Mobile Bookmarklets with the ShareTarget API

Paul Kinlan

Bookmarklets are an unsung superpower of Desktop Web Browsers. They are not as powerful as Chrome Extensions, they require a user gesture, they don’t have access to browser specific API’s (such as chrome.*), yet I love them. In their simplest form (a Bookmark) they navigate you to a URL; more complex forms (Bookmarklets) navigate to a javascript: URL which means you can run simple automated actions that run in the context of the page, creating functionality that the original developer didn’t get around to creating just yet, just at the click of a button.

Read More

Pixel 4XL Infrared sensor via getUserMedia

Paul Kinlan

It’s turns out that you can access the pixel 4 face detection IR camera via normal getUserMedia. The interesting thing is that if you try to do face unlock when this camera is being used (the bit where it throws things on to your face) the system will just flat refuse to do any of the face unlock process and that makes sense. If you want to quickly try this then you can view the demo here.

Read More

Sunset over Tokyo from Shibuya

Paul Kinlan

I was walking past the window in the Office and I saw this rather awesome view.

Read More

Harlech Castle

Paul Kinlan

I love a good Castle. Luckily North Wales is full of amazing Castles. Last week I took the boys to Harlech Castle and we had a grand old time. It’s well maintained and has an amazing history to it. It’s positioned on the top of a hill that has amazing views of Snowdonia and the Irish sea. Unlike Carlisle castle, I didn’t get to ruminate on the impact of Brexit, I just got to look at the awesome views.

Read More

Puppeteer Go🔗

Paul Kinlan

I love Puppeteer - it lets me play around with the ideas of The Headless Web - that is running the web in a browser without a visible browser and even build tools like DOM-curl (Curl that runs JavaScript). Specifically I love scripting the browser to scrape, manipulate and interact with pages.

One demo I wanted to make was inspired by Ire’s Capturing 422 live images post where she ran a puppeteer script that would navigate to many pages and take a screenshot. Instead of going to many pages, I wanted to take many screenshots of elements on the page.

The problem that I have with Puppeteer is the opening stanza that you need to do anything. Launch, Open tab, navigate - it’s not complex, it’s just more boilerplate than I want to create for simple scripts. That’s why I created Puppeteer Go. It’s just a small script that helps me build CLI utilities easily that opens the browser, navigates to a page, performs your action and then cleans up after itself.

Check it out.

const { go } = require('puppeteer-go');

go('https://paul.kinlan.me', async (page) => {
    const elements = await page.$$("h1");
    let count = 0;
    for(let element of elements) {
      try {
        await element.screenshot({ path: `${count++}.png`});
      } catch (err) {
        console.log(count, err);
      }
    }
});

The above code will find the h1 element in my blog and take a screenshot. This is nowhere near as good as Ire’s work, but I thought it was neat to see if we can quickly pull screenshots from canisuse.com directly from the page.

const { go } = require('puppeteer-go');

go('https://caniuse.com/#search=css', async (page) => {
    const elements = await page.$$("article.feature-block.feature-block--feature");
    let count = 0;
    for(let element of elements) {
      try {
        await element.screenshot({ path: `${count++}.png`});
      } catch (err) {
        console.log(count, err);
      }
    }
});
4.png
3.png
2.png
1.png
0.png

Enjoy!

A simple video insertion tool for EditorJS🔗

Paul Kinlan

I really like EditorJS. It’s let me create a very simple web-hosted interface for my static Hugo blog.

EditorJS has most of what I need in a simple block-based editor. It has a plugin for headers, code, and even a simple way to add images to the editor without requiring hosting infrastructure. It doesn’t have a simple way to add video’s to the editor, until now.

I took the simple-image plugin repository and changed it up (just a tad) to create a simple-video plugin (npm module). Now I can include videos easily in this blog.

If you are familar with EditorJS, it’s rather simple to include in your projects. Just install it as follows

npm i simple-video-editorjs

And then just include it in your project as you see fit.

const SimpleVideo = require('simple-video-editorjs');

var editor = EditorJS({
  ...
  
  tools: {
    ...
    video: SimpleVideo,
  }
  
  ...
});

The editor has some simple options that let you configure how the video should be hosted in the page:

  1. Autoplay - will the video play automatically when the page loads
  2. muted - will the video not have sound on by default (needed for autoplay)
  3. controls - will the video have the default HTML controls.

Below is a quick example of a video that is embedded (and showing some of the options).

Anyway, I had fun creating this little plugin - it was not too hard to create and about the only thing that I did was defer the conversion to base64 which simple-images uses and instead just use the Blob URLs.

Test post Video upload

Friendly Project Name Generator with Zeit🔗

Paul Kinlan

I’ve got some ideas for projects that make it easier to create sites on the web - one of the ideas is to make a netlify-like drag and drop interface for zeit based projects (I like zeit but it requires a tiny bit of cli magic to deploy).

This post covers just one small piece of the puzzle: creating project names.

Glitch is a good example of this, when you create a project it gives it a whimsical randomly generated name. The team also created a good dictionary of fairly safe words that combine well (and if you want they have a simple server to host).

So, the side project this Sunday was to create a simple micro-service to generate random project names using Zeit’s serverless-functions and the dictionary from Glitch.

And here it is (code), it’s pretty short and not too complex.

const words = require("friendly-words");

function generate(count = 1, separator = "-") {
  const { predicates, objects } = words;
  const pCount = predicates.length;
  const oCount = objects.length;
  const output = [];

  for (let i = 0; i < count; i++) {
    const pair = [predicates[Math.floor(Math.random() * pCount)], objects[Math.floor(Math.random() * oCount)]];
    output.push(pair.join(separator));
  }

  return output;
}

module.exports = { generate }

If you don’t want to include it in your project directly, you can use the HTTP endpoint to generate random project names (in the form of “X-Y”) by making a web request to https://friendly-project-name.kinlan.now.sh/api/names, which will return something like the following.

["momentous-professor"]

You can also control how many names to generate with the a query-string parameter of count=x, e.g. https://friendly-project-name.kinlan.now.sh/api/names?count=100

["melon-tangerine","broad-jury","rebel-hardcover","far-friend","notch-hornet","principled-wildcat","level-pilot","steadfast-bovid","holistic-plant","expensive-ulna","sixth-gear","political-wrench","marred-spatula","aware-weaver","awake-pair","nosy-hub","absorbing-petunia","rhetorical-birth","paint-sprint","stripe-reward","fine-guardian","coconut-jumbo","spangle-eye","sudden-euphonium","familiar-fossa","third-seaplane","workable-cough","hot-light","diligent-ceratonykus","literate-cobalt","tranquil-sandalwood","alabaster-pest","sage-detail","mousy-diascia","burly-food","fern-pie","confusion-capybara","harsh-asterisk","simple-triangle","brindle-collard","destiny-poppy","power-globeflower","ruby-crush","absorbed-trollius","meadow-blackberry","fierce-zipper","coal-mailbox","sponge-language","snow-lawyer","adjoining-bramble","deserted-flower","able-tortoise","equatorial-bugle","neat-evergreen","pointy-quart","occipital-tax","balsam-fork","dear-fairy","polished-produce","darkened-gondola","sugar-pantry","broad-slouch","safe-cormorant","foregoing-ostrich","quasar-mailman","glittery-marble","abalone-titanosaurus","descriptive-arch","nickel-ostrich","historical-candy","mire-mistake","painted-eater","pineapple-sassafras","pastoral-thief","holy-waterlily","mewing-humor","bubbly-cave","pepper-situation","nosy-colony","sprout-aries","cyan-bestseller","humorous-plywood","heavy-beauty","spiral-riverbed","gifted-income","lead-kiwi","pointed-catshark","ninth-ocean","purple-toucan","tundra-cut","coal-geography","icy-lunaria","agate-wildcat","respected-garlic","polar-almandine","periodic-narcissus","carbonated-waiter","lavish-breadfruit","confirmed-brand","repeated-period"]

You can control separator with the a query-string parameter of separator. i.e, separator=@ , e.g. https://friendly-project-name.kinlan.now.sh/api/names?separator=@

["handsomely@asterisk"]

A very useful aspect of this project is that if a combination of words tends towards being offensive, it is easy to update the Glitch repo to ensure that it doesn’t happen again.

Assuming that the project hosting doesn’t get too expensive I will keep the service up, but feel free to clone it yourselves if you ever want to create a similar micro-service.

Live example

What follows is a super quick example of the API in action.

const render = (promise, elementId) => {
  promise.then(async(response) => {
    const el = document.getElementById(elementId);
    el.innerText = await response.text();
  })
};


onload = () => {
  render(fetch("https://friendly-project-name.kinlan.now.sh/api/names"), "basic");
  render(fetch("https://friendly-project-name.kinlan.now.sh/api/names?count=100"), "many");
  render(fetch("https://friendly-project-name.kinlan.now.sh/api/names?separator=@"), "separator");
}

Single response



Many resposnses



Custom separators










  
        
      

Frankie and Bennys: Pay for your meal via the web

Paul Kinlan

Whenever I see a restaurant say that you can pay on mobile, I always check it out, mostly so I can bemoan the fact that you need to use an app. Imagine my surprise when the QR code lead to a web-based payments flow….. and it worked. Awesome work Frankie and Benny’s! At this point, I did select Google Pay, but it didn’t work (email internally sent!

Read More

Podroll

Paul Kinlan

I love podcasts and I listen to quite a few, yet I still find discovering new podcasts is quite a hard process and I rely on friends to share what the listen to. So with that in mind here is my podroll. I use Player.fm which is made by my good friend Mike Mahemoff and has the ability to share and export your subscription list. This page will be refreshed frequently using my little script.

Read More

Adding "dark mode" to my blog

Paul Kinlan

I saw Jeremy Keith’s post about adding dark mode to his blog and it seemed simple, so I decided to give it a whirl. Here is the diff of the work for all to see. It was surprisingly easy (outside of silly errors on my part). There was a small refactor to support CSS variables and ensuring I have fallback if there’s a browser that doesn’t support CSS custom properties, but that is about it.

Read More

Using Web Mentions in a static site (Hugo)

Paul Kinlan

My blog is an entirely static site, built with Hugo and hosted with Zeit. This is a great solution for me, a simple blog has a pretty simple deployment process and it loads blazingly fast. Statically generated sites do have some drawbacks, the largest is when you need anything dynamic to be integrated into your page (comments for example).  Not being able to easily host dynamic content will mean that you end up relying on 3rd party JavaScript that will then get full access to your page and you won’t know what it is doing - it could be tracking your users or slowing down your page load.

Read More

Creating a pop-out iframe with adoptNode and "magic iframes"

Paul Kinlan

Update: 8th October - Significant issues with this doc. I caught up with Jake Archibald about this post because I thought I had something novel, during the conversation we uncovered a lot of things that make some of this post invalid, and I also learnt a lot in the process that I don’t think most developers know. Calling .append() and .appendChild() adopt the node. This makes the usage of adoptNode in this instance useless because the append Algorithm ensures that the node is adopted.

Read More