Using Visual Studio Code (VSCode) to debug Parcel and Gatsby projects

Being able to properly debug projects within your favorite editor/IDE is an essential part of being a developer. In this post I’ll take you through setting up Visual Studio Code (VSCode) in way that allows you to debug your code directly within VSCode when working with the page generator Gatsby ( and projects build with the Parcel bundler ( This works for both Typescript and regular JavaScript projects.

Debugging projects generated with the Parcel bundler

The goal here is to be able to set breakpoints in our code files in VSCode then launch Chrome from VSCode and perform the debugging steps all from within the VSCode editor. This should work with Typescript projects, JavaScript projects and React Projects.


  • First you’ll need to install the “Debugger for Chrome” extension for Visual Studio Code (msjsdiag.debugger-for-chrome)
  • The next step is to configure a launch configuration (see below).
  • Add an npm script to start parcel, e.g: "start": "parcel src/index.html".
	"version": "0.2.0",
	"configurations": [
			"type": "chrome",
			"request": "launch",
			"name": "Launch Chrome debugger",
			"url": "http://localhost:1234",
			"webRoot": "${workspaceFolder}",
			"breakOnLoad": true,
			"sourceMapPathOverrides": {
				"../*": "${webRoot}/*"


  • Run the npm script first, in this example it would be npm start
  • Now you can start debugging in VSCode by running «Launch Chrome debugger» from the «Run and debug» menu
  • Chrome will open and you can set breakpoints within VSCode

Debugging Gatsby projects within Visual Studio Code (VSCode)

There are two separate steps to a Gatsby project you might like to debug, the build phase and/or the runtime phase. The full launch config for both phases can be found at the bottom of this post.

Debugging the Gatsby build phase from Visual Studio Code (VSCode)

The build phase in a Gatsby project is when Gatsby is generating all pre-generated content, such and pages etc. In this phase you’ll mainly focus on debugging the «gatsby-node.[js|ts]» file, and of course any file you refer to. Since this step in Gatsby is executed by Node, this is just like debugging a Node application. Works for both JS and Typescript Gatsby projects.

  • If you are using Typescript, set "sourceMap": true, in the «tsconfig» file.
  • Add a new config to your launch.json config file in VSCode (see below)
  • Set a breakpoint in your «gatsby-node» file and run the launch config below
	"name": "Debug gatsby-node",
	"type": "pwa-node",
	"request": "launch",
	"program": "${workspaceRoot}/node_modules/.bin/gatsby",
	"args": ["develop"],
	"stopOnEntry": false,
	"runtimeArgs": ["--nolazy"],
	"console": "integratedTerminal"

Debugging the runtime React part of a Gatsby project from within Visual Studio Code (VScode)

The runtime phase is where you’ll want to debug any client side React and Typescript/JavaScript code. This setup is very similar to the Parcel setup at the start of this post. Let’s go though the steps:

  • If you haven’t done this already, install the “Debugger for Chrome” extension for Visual Studio Code (msjsdiag.debugger-for-chrome)
  • The next step is to configure a launch configuration (see below).
  • Setup a npm script to start the Gatsby server: "start": "gatsby develop"
	"type": "chrome",
	"request": "launch",
	"name": "Launch Chrome debugger",
	"url": "http://localhost:8000",
	"webRoot": "${workspaceFolder}"
Debugging the runtime React app
  • To debug you can now run the npm script npm start and when it’s done (might take a while) …
  • Now you can start debugging in VSCode by running «Launch Chrome debugger» from the «Run and debug» menu

Full launch config for both Gatsby phases

	"version": "0.2.0",
	"configurations": [
			"name": "Debug gatsby-node",
			"type": "pwa-node",
			"request": "launch",
			"program": "${workspaceRoot}/node_modules/.bin/gatsby",
			"args": ["develop"],
			"stopOnEntry": false,
			"runtimeArgs": ["--nolazy"],
			"console": "integratedTerminal"
			"type": "chrome",
			"request": "launch",
			"name": "Launch Chrome debugger",
			"url": "http://localhost:8000",
			"webRoot": "${workspaceFolder}"

QuickTip: Css only tool tip

Tool tips can sometimes be useful to display things like help texts and similar. This post demonstrates how to create a tool tip without any JavaScript, only using HTML and CSS.

Example - Tooltip on hover

This example uses the hover event. This does work (isj) even on touch devices, but it’s a good idea to provide a fallback.

	<a href="" data-tip="This is a tooltip"> Hover over me. </a>

The html is just an anchor tag inside a div. The anchor tag has a data attribute called tip, this is will be used as the source for the tooltip content.

First selector - «a[data-tip]»

All of the CSS rules uses attribute selectors. You don’t need to use attribute selectors, but it makes it easier to target only those a elements that has tool-tip data attached.

a[data-tip] {
	position: relative;
	color: #00f;

This selector targets the anchor tag itself. The only required property here is position:relative, this is needed to gain a working starting position for the tooltip.

Second selector - «a[data-tip]:hover/focus::after»

a[data-tip]:focus::after {
	content: attr(data-tip);
	position: absolute;
	left: 0;
	top: 24px;
	min-width: 200px;
	border: 1px black solid;
	border-radius: 5px;
	background-color: rgba(0, 0, 0, 0.5);
	padding: 12px;
	color: white;
	font-size: 14px;
	z-index: 1;

This is where it all happens. The main thing making this work is the ::after pseudo selector. When used with the content property it will insert a pseudo element as the last child of the selected element. In this example it adds the tooltip text as a child element to the anchor tag.

  • «content: attr(data-tip)» sets the content of the tooltip to the content in the «tip» data attribute.
  • «position:absolute» and the «left/top» values sets the position of the tooltip relative to the anchor tag
  • The rest of the css is mostly styling the tooltip.

How does this example work on touch-devices?

Hover doesn’t exist on touch devices. However, browsers have implemented the hover action to trigger on first tap. So, when the user taps the hover target, the hover event will trigger, on the next tap any other events (like click for links) triggers.

References and links

QuickTip: Using CSS «line-clamp» to truncate text to a set number of lines

When working with dynamic text content it can be quite useful to be able to truncate the number of text lines displayed in a HTML element. This can be done using the CSS «line-clamp» property.

The resulting line trucation
The resulting line trucation

Consider the HTML below. We have a div with a paragraph inside containing some text. For this demo we want to show max 3 lines of text in this div element.

<div class="content">
		Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec venenatis velit sit amet dui semper dignissim.
		More dynamic text here.

Lets say the max width of the content div is 200 pixels and max of 100 pixels in height.

.content {
	width: 200px;
	height: 100px;
	padding: 10px;

We want to truncate all text over 3 lines. Here we set the display to «-webkit-box» and the <code>«-webkit-line-clamp»</code> property to 3 lines of text.

.content p {
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
	overflow: hidden;


QuickTip: Matching the local version of Hugo on AWS Amplify

When using Hugo to generate a static website it is quite important to match the Hugo versions locally with the version used by AWS Amplify.

If you don’t you can end up in a situation where for example syntax highlighting is working fine locally, but isn’t working on the deployed site. This happened to me, and this is how I fixed it.

  1. Open your AWS Amplify deploy project and choose «Build Settings» from the left menu.
  2. Scroll all the way down to «Build image settings» and click the edit button on the right side.
  3. Add a package version override
    1. Select “Hugo” as the package
    2. The version should be set to “latest” (or the version you prefer).
    3. Save

Now Amplify will use the version of Hugo you specified for all future builds. You’ll need to trigger a new build for any changes to your site to take effect.

PS: Remember to keep your local version of Hugo updated to the same version you selected in the Amplify console.


JAMStack: Integrating and Hugo

In this article I’m discussing my approach to creating a so-called JAMStack site, using this site ( as an example. The site is integrated with a CMS called Sanity for easy content management.


I wanted something simple, cheap, fast (load-times) and secure. After testing several combinations of CMS’es and static site generators I landed on as the CMS and Hugo as the static site generator.

Sanity is the platform for structured content. With you can manage your text, images, and other media with APIs. Source: The website

I chose Sanity because I like the approach to defining data structures as code. This makes it much easier to keep a backup and redeploy the CMS to new instances. Sanity is also responsive, great to work with and have a reasonable pricing model.


Hugo is one of the most popular open-source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again. Source: Hugo website

The main reason I went with Hugo instead of the massively popular Gatsby was build-speed and the approach to building pages. Building templates in Hugo consists mostly about writing HTML and CSS. In Gatsby everything is build using React, something which can be an advantage if you need to mix build time and runtime react components. I had no such need since a wanted a static blog site. Then there is the build speed… Hugo is way..way faster than Gatsby. This matter a great deal when you need for the entire site to build every time you click «publish» in the CMS.

With the setup explained below, deploying this site takes about 1 min and 30 seconds from clicking publish to the change is visible

AWS Amplify

For building and deploying the site I’m using AWS Amplify Deploy which has build in support for Hugo, Node.js and is powered by the AWS Cloudfront CDN, which makes things fast and cheap to host. You can probably use Netlify or similar achieving the same results.

The setup

Normally when using Hugo you would store all the content in a folder named content at the root level. The structure within the content folder would then define the url structure of the resulting site. For example a blog posts with the url technology/2020/01/28/something would mean that the markdown file was store at the /content/technology/2020/01/28 path.

// «/content/technology/2020/01/28/something}»

└── technology/
   └── 2020/
      └── 01/
         └── 28/

Since the content is now stored in Sanity we need a way to import the content from Sanity into the project at build time. For doing this I wrote a small JavaScript file which is executed by Node.js at build time. The script uses the Sanity JS client with a GROQ query (see example below).

In my case the query covers both the technology blog and the work blog in one request. This might have to be tweaked for sites with a large content base by adding pagination.

// PS: This query is based on my Sanity schema

const query = `*[_type == "workPost" || _type == "techPost"] {

You would then go through each post and generate the appropriate markdown file at the correct path in the content folder.

// Again, this is my setup, yours will probably differ
const slug = fields.slug.current;
const date = new Date(fields.publishedAt);
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");

// The full folder path for the markdown file
const dirPath = path.join(contentFolderPath, dirName, year, month, day);
// The name of the markdown file
const fileName = `${slug}.md`;

// The markdown content with front-matter
const content = `---
title: '${fields.title}'
date: ${fields.publishedAt}
draft: false
slug: "${slug}"
description: '${fields.metaDescription}'


try {
	// Create the full path for the markdown file
	// This can be multiple folders, hence the recursive flag
	await fs.promises.mkdir(dirPath, { recursive: true });
} catch (error) {
	// Ignore the folder exists error since this will
	// happen if one day has multiple posts
	if (error.code !== "EEXIST") {
		throw error;

try {
	// Write the markdown file
	await fs.promises.writeFile(path.join(dirPath, fileName), content);
	return console.log(`✅ Wrote file ${fileName}`);
} catch (error) {
	throw error;

When the script has completed, which will probably take less than a second unless you have many many posts, then you’ll run Hugo build and wait for Amplify to do its thing.

The full build setup looks something like this.

The build setup
The build setup

A complete amplify config file can look like this

version: 0.1
				# Install node modules for the importer script
                - yarn install
                # Import data from Sanity and create Markdown files
                - node sanityToMarkdownImporter.js
            	# Run the Hugo build
                - hugo --minify
        baseDirectory: public
            - "**/*"
    	# Cache node_modules for next deploy
        paths: [node_modules/]
        # Cache css in the browser for a year
        - pattern: "**/*.css"
          headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
        # Cache images in the browser for a year
        - pattern: "**/*.gif"
          headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
        - pattern: "**/*.jpg"
          headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
        - pattern: "**/*.png"
          headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]
        - pattern: "**/*.svg"
          headers: [{ key: "Cache-Control", value: "public,max-age=31536000,immutable" }]

The result is .. well, the site you’re reading now. It loads fast, it is cheap to host and works pretty well.