Hello. I am Paul Kinlan.

I lead the Chrome and the Open Web Developer Relations team at Google. Exploring the intersection of modern web design and future-facing technologies.

1 min read

Matsushima, Miyagi

Matsushima, Miyagi is a beautiful seaside town an hour from Sendai. Famous for its fresh oysters, islands, and red bridges, it's a charming place to visit. While the 2011 tsunami impacted the area, the town has recovered remarkably well. I found the quiet, cold atmosphere when I went added to its appeal.

Stay in the loop.

I'm trialing a newsletter. Join for monthly insights into web dev, Chrome, and the open web.

alternate_email

Get in touch

Open to chat about Chrome or Web development.

Book a consultation
1 min read

Yamadera, Yamagata

I took a day trip to the 1000-year-old Yamadera Temple in Yamagata, Japan. The climb to the top wasn't too difficult and offered breathtaking views of the valley. It was a quiet day with few other visitors, unlike busier weekends and holidays. The oldest building there is around 400 years old.
3 min read

Modern Mobile Bookmarklets with the ShareTarget API

Mobile devices lack the bookmarklet functionality found in desktop browsers. However, the ShareTarget API offers a potential workaround. This API allows web apps to be installed and receive native share actions, similar to how the Twitter PWA handles shared links and files. By leveraging this API, developers can create mini-apps that perform actions on shared data. This approach involves defining how to receive data in a manifest file and handling the request in a service worker. I've created examples for Hacker News, Reddit, and LinkedIn demonstrating how to utilize the ShareTarget API. While not a perfect replacement for desktop bookmarklets, this offers a new level of hackability for mobile web experiences.
1 min read

Pixel 4XL Infrared sensor via getUserMedia

The Pixel 4 XL's infrared camera, used for face detection, can be accessed through the standard getUserMedia API. A live demo showcasing this can be found at the provided link. Using the IR camera via getUserMedia blocks the phone's face unlock feature. This post invites readers to brainstorm potential applications of user-accessible infrared camera capabilities. An update mentions Francois Beafort's contribution to Blink, adding 'infrared' to the camera name if the device supports it, making camera identification more convenient.
1 min read

Harlech Castle

Had a wonderful time exploring the magnificent Harlech Castle in North Wales with my kids. The castle is well-preserved and steeped in history, perched atop a hill with breathtaking views of Snowdonia and the Irish Sea. Unlike my previous visit to Carlisle Castle, this trip was purely focused on enjoying the stunning scenery.
2 min read

Puppeteer Go

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!

2 min read

A simple video insertion tool for EditorJS

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.

2 min read

Friendly Project Name Generator with Zeit

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