Set Up a PHP-based Website on Heroku

I’ve decided to move my PHP-based websites from GoDaddy shared hosting to Heroku (PaaS / Platform as a Service). Here are the steps I followed so if I need a quick reminder of how I did it, I can just look here. I decided not to go with IaaS (Infrastructure as a Service) options like AWS, Azure, and GCP (Google Cloud Platform) because they are overkill for my needs, are more complicated, and, as a developer, I want to focus on development, not infrastructure.

1. Get PHP Locally

Download PHP. Since I’m on Windows, I downloaded the VS16 x64 Thread Safe (2022-Dec-06 16:15:24) Zip file. I then extracted it to C:\php-8.2.0-Win32-vs16-x64

2. Update Path

Add the PHP path to your system PATH system or user environment variable.

Verify the updated Path environment variable is loaded by running the following command on the command line.

Get-ChildItem Env:Path | Format-Table -Wrap -AutoSize

If you see the PHP path in the output, then the updated Path environment variable has been loaded. If you don’t see it, then restart Explorer or your computer (there may be easier ways to load updated environment variables).

Test PHP by checking the version on the command line. Run the following command:

php -v

If you see the PHP version, then PHP is working.

3. Start a web server

PHP comes with a built-in web server. In your project folder, run the following command.

php -S localhost:8000

You can then open your PHP website in a browser at http://localhost:8000/.

4. Install PHP Composer

Composer is a dependency manager for PHP. Even if you don’t need it, it’s required by Heroku. Heroku will know that your website runs PHP by detecting the presence of a file composer.json.

Since I’m on Windows, I’ll download and run Composer-Setup.exe. It will install the latest Composer version and set up your PATH so that you can call composer from any directory in your command line. Follow the instructions to install Composer. Then, verify it is loaded by running the following command to check is version.

composer -v

5. Add composer.json file

Create a file in the root of your project folder called composer.json. In my case, I don’t need any dependencies so I will just add {} to the file which is an empty JSON object. (In my case, I guess I didn’t really need to install composer since my composer.json file is empty).

6. Commit to git

This step assumes you have git installed. In my case, I will also commit my project to GitHub.

  1. Create a new repository in GitHub
    To avoid errors, do not initialize the new repository with README, license, or gitignore files. You can add these files after your project has been pushed to GitHub.
  2. Initialize the local directory as a git repo. Run git init -b main in our project folder. This will create a hidden .git folder.
  1. Add the files in your new local repository. This stages them for the first commit.
git add .
  1. Commit the files that you’ve staged in your local repository.
git commit -m "First commit"
  1. At the top of your repository on’s Quick Setup page, click to copy the remote repository URL. In the Command prompt, add the URL for the remote repository where your local repository will be pushed.
$ git remote add origin <REMOTE_URL> 
# Sets the new remote 
$ git remote -v 
# Verifies the new remote URL

5. Set tracking information for the main branch

git branch --set-upstream-to=origin/main main

6. Push changes to GitHub

git push origin main

7. Add gitignore file

Create a .gitignore file in the root of your project folder and add the following lines.


That way, when you commit files, you will not commit the .env file, if any, and any files in the vendor folder, if any.

8. Create a procfile

Create a file in the root of your project folder called Procfile. It should not have an extension. Add the following line to it.

web: vendor/bin/heroku-php-apache2

This will tell Heroku to set up a web server using Apache and PHP. If you want nginx instead of Apache, add the following line instead.

web: vendor/bin/heroku-php-nginx

9. Install Heroku CLI

The Heroku CLI (Command Line Interface) is necessary to push (publish) changes to your website to Heroku. Since I’m on Windows, I’ll download the Windows installer. The installer will add Heroku to the Path environment variable.

To load the updated environment variable, close all Explorer and command line / PowerShell windows. Reopen a command line / PowerShell and type enter the following

heroku login

A browser tab will open asking you to log in to Heroku CLI. Log in.

Once you’ve logged in you, you can close the browser tab and return to the command line.

The command line will get updated to show that you’ve logged in.

10. Deploy your website

Create an app on Heroku, which prepares Heroku to receive your source code. Run the following command.

heroku create

When you create an app, a git remote (called heroku) is also created and associated with your local git repository.

Heroku generates a random name (in this case sharp-rain-871) for your app, or you can pass a parameter to specify your own app name.

To deploy your website, run the following command.

git push heroku main

11. Run the app

Now that the website has been deployed, ensure that at least one instance of the app is running. Run the following command.

heroku ps:scale web=1

12. View the website

heroku open

The website will magically open in a browser.

13. Make changes

Whenever you make changes to the website, commit and push them to Heroku. To simplify deployments, you can tell Heroku to use GitHub instead of the Heroku CLI.

A Selection of Useful Reports in Google Analytics

Having a website without a reporting tool like Google Analytics (GA) is like flying blind. The data available is very useful, if not critical, in making business decisions. Yet, it surprises me how many people continue to make website changes based on pure assumption and without considering user behavior and traffic data at all.

Before looking at various reports, it’s important to understand some background information.


  • Pageviews: # of times a page was viewed, including by the same visitor. ​
  • Unique Pageviews: # of times a page was viewed by unique visitor sessions​
  • Avg Time on Page: The higher the avg time on page, the more the user is engaged with the content. If pageviews is high but avg time on page is low, then people probably aren’t finding what they are looking for.​
  • Bounce Rate: A “bounce” is when someone lands on from an external source and leaves without clicking on any internal links. They see only one page. Lower bounce rates are better, but a high bounce rate may be reasonable, depending on intent and context. For example, blogs tend to have higher bounce rates (70-90%)[1] than e-commerce/retail websites (20-45%)[2] because people read an article and leave whereas on retail sites, they tend to browse multiple product pages while shopping.​

Visitor Intent

When interpreting data, it’s important to understand visitor intent. ​

  • Informational: people intend to find an answer to a specific question or general information. If a blog post has a high bounce rate but high avg time on page, that is to be expected because people come for information and nothing more.​
  • Commercial: people intend to investigate brands or services. If people visit Qualys product pages, spend time reading the page, but bounce instead of sign up for a trial, that is reasonable if they are still investigating/comparing brands/products.​
  • Transactional: people intend complete an action or purchase. ​
  • Navigational: people intend to find a specific site or page. If people come to the Qualys home page and the avg time on page is < 10 secs, that is to be expected if they know they are looking for a specific product page.

Channels (Traffic Source)

When interpreting data, it’s important to know where traffic came from. ​

  • Direct: Source is unknown or visitor typed the URL directly in a browser ​
  • Organic Search: Source is from non-paid search engine results, e.g. Google​
  • Paid Search: Source is from paid search engine ads, e.g. Google CPC​
  • Social: Source is a social network, e.g. LinkedIn, Facebook, etc​
  • Paid Social: Source is from paid ads on a social network, e.g. LinkedIn, Facebook, etc​
    • Email: Source is an email​
  • Affiliates: Source is an affiliate/partner​
  • Referral: The visitor came from some other website​
  • Display: Source came from ad distribution network, e.g. banner ads on some websites​
  • Other: Visitor came from some other source​ ​

Page Analysis FAQs​

Q: How do I know if people are finding a particular page?​

A: If the page has many unique pageviews, then many people are finding it.​

Q: How do I know if people are engaged in consuming the content of a particular page?​

A: If the avg time on page is high, then we can assume people are engaged in it (reading, watching videos, etc).​

Q: How do I know where people are finding a particular page?​

A: Look at the page’s source / medium, e.g. Google / organic, Email, Social, etc​

Q: How do I know what people are clicking on on a particular page?​

A: Look at the page’s next page path (page flow).​

Q: One of my pages is long. The avg time on page is high but how do I know people are scrolling down to see the whole page?​

A: Look at the page’s scroll depth. It will tell how what percentage of visitors see 25%, 50%, 75% and 100% of the page.

Now, let’s look at some reports in Google Analytics that are interesting and useful and that can be used to make data-driven decisions about website changes.

Audience > Geo > Language

Knowing what language your users speak can help you make a decision on which languages you should have your website in. GA gets language data from a user’s browser which are in specific language-country codes. en-US is English-US, en-gb is English-Great Britain, en-ca is English-Canada, and so on. In the screenshot below, we see that most users have their browser language set to some variant of English, but some (1.05%) have it set to zh-cn (Chinese-People’s Republic of China), de-de (German-Germany), and fr-fr (French-France).

Audience > Geo > Country

Google Analytics tracks user location based on their IP address. It’s always interesting to see where your website visitors are viewing your site from. In the screenshot below, almost 8% of website visitors are in India yet, compared to the language report above, hi-IN (Hindi-India) wasn’t in the list. I guess people in India set their browser language to English (en).

Audience > Behavior > New vs Returning

This report compares how many visitors are new versus returning. If many people are returning visitors to your website, then the website must have a lot of interesting content that makes them want to keep returning.

Audience > Behavior > Frequency & Recency


If you click the “Count of Sessions” distribution, you will see how frequent the same individual visits your site in a given time period. For example, in the report below, 720 people visited the website 3 times (e.g. once a week over 3 weeks).

If you click the “Days Since Last Session” distribution, you will see how long it’s been since your visitors last came to your site in a given time period. For example, in the report below, it has been 2 days since 223 people last visited the website.


Audience > Behavior > Engagement

Session Duration Bucket

This report tells you how many visitor sessions are within a particular duration. For example, in the report below, there were 1204 visitor sessions that lasted between 601 and 800 seconds.

Page Depth

This report tells you the number of sessions where the page depth (number of pages visited) was 1, 2, etc. In the report below, there were 678 sessions where the visitors visited 3 pages during their session.

Audience > Technology > Browser & OS


In this report, you can see what percentage of users use a particular browser. In the report below, more than 50% of users use Chrome. So, if your website has a feature that is broken in Chrome, you should fix it. We also see that 0.58% of visitors use the Opera browser. So, if your website doesn’t work in Opera, who cares. Well, 0.58% of your visitors might care but so what.

Operating System

In this report, you can see what percentage of your visitors use a particular operating system. In the report below, the majority of visitors (30.41%) visit the website on iOS.

Screen Resolution

In this report, you can see what percentage of your visitors view your website at a particular screen resolution. In the report below, we see that 10.88% of people view the website on desktop at a resolution of 1920×1080. Many others view the site at screen widths below 500px. This means they are probably viewing the site on their phones. Notice how the smallest screen resolution is 360×800. In this case, you would want to ensure the mobile version of your site looks good at a width of 360 px.

Audience > Mobile > Overview

Device Category

In this report, we can see what percentage of people are visiting your site on mobile, desktop and tablet. In the report below, very few people (1.63%) view the site on tablet so ensuring the site looks good on tablet isn’t particularly important. However, the majority of visitors visit the site on mobile so it’s important to ensure the site looks good on mobile.

Audience > Mobile > Devices

In this report, we can see what percentage of people visit your site on a particular device (iPhone, iPad, etc). In the report below, more than 50% of the website visitors visit the site on mobile using an Apple iPhone.

Audience > Users Flow

In this report, you can see how people flow through your site. The first column is the primary dimension. In the report below, the primary dimension is “Country”.

Of the 17K sessions from the US, 1.6K land (start) on the home page, and from there, they go to some other pages shown in the 1st Interaction column.

If we click another band, we can see that of the 17K visitors from the US, 1.7K land (start) on the blog post about grilling corn using a Ninja Foodi grill.

You can also change the primary dimension to something else like “Landing Page”.

Acquisition > All Traffic > Channels

In this report, we can see what percentage of users visit the site from different channels (organic search, direct, social, etc). In the report below, 77.24% of visitors find the site from organic search, e.g. searching from Google.

If you click “Organic Search”, you’ll see another report like the one below. The default primary dimension is keyword. Almost all of the keywords used to find the site in a search engine have value of “(not provided)” or “(not set)”. Obviously, this is incorrect because people must have searched for something to find the site in Google. The reason why it says “not provided” or “not set” is because Google decided to hide this keyword data to protect users’ privacy.

If you click on the “Source” primary dimension, you can compare visits by search engine. In the report below, clearly, the majority of visitors found the site from Google.

Acquisition > All Traffic > Source/Medium

Sometimes, it’s more helpful to see how people are finding your website by a combination of source and medium as shown in the screenshot below.

Behavior > Behavior Flow

This report is similar to the Users Flow report mentioned above. You can see how people flow / traverse through your website as they click from one page to another.

Behavior > Site Content >All Pages

When the primary dimension is set to “Page” (default), in this report you can see traffic and behavior by page, e.g. how many pageviews, unique pageviews, average time on page, entrances, bounce rate, and % exit a particular page got. This report clearly tells you your most visited pages.

Secondary Dimension

The secondary dimension option allows you to see reports in other ways. This is extremely useful. For example, let’s say you have a registration page and you want to promote it by linking to it from many places, both internally on the same website, e.g. on your home page or a product page, and externally on other websites.

Internal referrals

To find which pages on your website are generating traffic directly to a particular page on your website, first, type part of the page URL in the filter field and click the search button. This will filter out all other pages and only show the specific page you are interested in

Then, in the secondary dimension dropdown, choose Behavior > Previous Page Path.

Now, in the resulting report (below), we can see that for the specific page we’re interested in, we find that 88% of people landing on that page (entered it from some other website like from Google search results). 0.6% got to that page from the home page and another 0.18% got to it from the About page.

External referrals

To find how people got to that page from an external source, change the secondary dimension to Acquisition > Source / Medium.

Now we see that 76% came from Google organic search results and 20% directly, e.g. they typed the URL in from somewhere or clicked a link in an email.

Next Page Path

What if we want to know what pages people are getting to from, say, the home page? If we change the filter to just “/” (the home page) and set the secondary dimension to “Next page path”, we see the following report.

The report above looks wrong because it says that 100% of visitors went from the home page to the home page (next page). To find out the correct next pages, click the Navigation Summary tab.

Now we can see what pages people came from before hitting the home page and what pages people went to after hitting the home page.

There are many other secondary dimensions so it’s worth spending time browsing the different types of reports you can get from other dimensions. For example, you can see how many people visited a specific page by country.

Behavior > Site Content >Content Drilldown

In this report, you can see pageviews (and other data) by folder structure (URL structure). For example, in the report below, there were 21,503 pageviews of pages in the 2019 folder (blog posts published in 2019).

If you click a folder like /2019/, you can drill down to subfolders. In the report below, the subfolders are numbers indicating months. For example, in the report below, there were 18,267 pageviews to pages in /2019/09/.

Behavior > Site Content >Landing Pages

This report is similar to the one under Behavior > Site Content > All Pages, but this one is specifically for landing pages, i.e., the pages through which visitors entered your site. You can think of “landing pages” as “entrance pages”.

Behavior > Site Content >Exit Pages

Opposite of landing pages is exit pages, i.e. pages from which people exited your site.

Behavior > Site Speed > Overview

In this report, you can see how fast your website loads on average by browser, country, and page. Of course, you want your website to load as fast as possible. In the report below, we see that the average page load time is 5.78 seconds. Interestingly, the average page load time in Chrome is much higher (7.15 sec) compared to Safari (3.33 sec).

If we look at the average page load time by country, we see that the website loads the fastest (1.53 sec) from Slovenia for some reason.

Behavior > Site Speed > Page Timings

In this report, we can compare the average page load time of individual pages against the site average page load speed. This is helpful to find specific pages that aren’t performing well, e.g. the one with the red bar in the screenshot below.

Behavior > Events > Top Events > Scroll

If you’ve added an event category of “Scroll”, you can see what percentage of people scrolled 25%, 50%, 75% and 100% down a page. In the example below, 46% of people only saw the top 25% of the home page and only 7% of people scrolled all the way to the bottom.

Form Backends for Static Websites

If you’re getting on the Jamstack bandwagon, you’ll probably get to a point where you need to figure out a way to handle web forms. That’s what happened to me when I migrated from WordPress (PHP) to static HTML. I needed a way to handle my contact form. Fortunately, there are many form backend services like

After reviewing each one, I find KwesForms to be the best, but to have one of the worst costs because it’s one of the most expensive.

At $29 / month, that’s cheap for a company but more than I would want to pay for a personal blog. There is a free version but it has some limitations and has the KwesForms logo on confirmation emails. Anyway, KwesForms has the best features, what super easy to integrate, includes form validation, custom redirects, clear documentation, and more. You can view and edit form data and export it all as a CSV file.

Since I don’t want to pay $29 / month for a contact form, and since I’ll be hosting my new blog on Netlify, I’m just going to use Netlify Forms. It’s not as user-friendly and feature-packed as KwesForms, which is to be expected since Netlify specializes in static site hosting, not form handling, but it’s free (up to 100 submissions per month). However, unlike KwesFroms, which comes with form validation, I’ll have to add my write my own form validation code. Instead of reinvent the wheel, I’ll use .validate, a jQuery validation plugin.

As you can see from their website and the video below, it’s dead simple to use.

Website Speed Comparison: WordPress (PHP) on GoDaddy vs Static HTML on Netlify

I’m in the process of migrating this blog from a managed WordPress instance on GoDaddy to a static HTML site on Netlify. Before I switch over the domain, I wanted to compare the Google Lighthouse performance scores for each site. In Google Chrome Developer Tools, I clicked the Lighthouse tab and ran a test for each site. Here are the results.

Performance score for WordPress (PHP) version of site on GoDaddy

Performance score for static HTML version of site on Netlify

Now, you might be thinking, why would the PHP site get a slightly higher score than the static HTML site? The static site has a lower cumulative layout shift score (see definition below). This factor has nothing to do with PHP vs HTML or GoDaddy vs Netlify. This factor has to do with how elements on the page shift their position. The original theme in the WordPress site came from one developer and the theme in the static HTML site came from another developer who recreated the original theme. This tells me that the original theme was coded better than the recreated theme. Since I’m more concerned with speed rather than layout shift, we can see that the static HTML site on Netlify is much faster than the PHP site on GoDaddy. This is to be expected. If we add up all scores except for the Cumulative Layout Shift score, we get

  • PHP on GoDaddy Performance Score: 3.9 s
  • HTML on Netlify Performance Score: 1.3 s

The new site performs 3x faster than the old one! That’s a speed gain of 300%!

First Contentful Paint

First Contentful Paint marks the time at which the first text or image is painted. Learn more.

Time to Interactive

Time to interactive is the amount of time it takes for the page to become fully interactive. Learn more.

Speed Index

Speed Index shows how quickly the contents of a page are visibly populated. Learn more.

Total Blocking Time

Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. Learn more.

Largest Contentful Paint

Largest Contentful Paint marks the time at which the largest text or image is painted. Learn more

Cumulative Layout Shift

Cumulative Layout Shift measures the movement of visible elements within the viewport. Learn more.

11ty: Ways to Debug Data

I’m in the process of migrating this blog from Managed WordPress on GoDaddy to Eleventy, GitHub and Netlify. Since I like the convenience of writing content in WordPress, I decided to continue to use it, but just as a headless CMS. When 11ty builds the site, it would fetch WordPress post, page, category, tag, and author data and, using the eleventy-fetch plugin, cache the data locally for a customizable period (I chose 1 day). Since Netlify automatically triggers a build when it detects a commit or push to GitHub, an automated build only happens when I make changes that are tracked by git and not changes in WordPress. For WordPress changes that I want published, I would manually trigger a build in the Netlify admin panel.

During this migration project, there were coding bugs that needed to be fixed. Following are some of the ways I discovered helped me to debug in Eleventy.

Debugging data in JavaScript files in the _data folder

As mentioned above, my project fetches data from WordPress to dynamically build pages. For example, I have a file at _data/authors.js that fetches data from the WordPress remote API endpoint, does some custom processing, and returns the data. It is often necessary to see if the data at different points of the code is as expected. To see this data, you can simply console out the data variable (see line 53 in the screenshot below).

When 11ty builds the site, the console.log statement will output the data in the console / terminal. However, f you are running the default 11ty build command, depending on how many files are being built (in my case, 11ty wrote 1015 files), the data dump may get truncated or lost in the console output. To remove the noise and status messages in the 11ty output, enable quiet mode. Since I’m on Windows, I use the following command.

npx @11ty/eleventy --serve --incremental --quiet 

Now, the output is much simpler, and I can see the data dump immediately.

Debugging data in template files

If you’d like to view the value of data variables in template files, you can do that by outputting the value and passing it to the log function (see example on line 9 below). This will tell 11ty to output the data to the console / terminal.

Dumping all data to a built page

Viewing data in the console terminal is handy for some situations. But, sometimes you can have a lot of JSON data that you’d like to see. In this case, it can be easier to dump the data to a page that you can view the entirety of in a file or at a URL To do this, first add a filter with the following code to .eleventy.js.

eleventyConfig.addFilter('dump', obj => {
    const getCircularReplacer = () => {
      const seen = new WeakSet();
      return (key, value) => {
        if (typeof value === "object" && value !== null) {
          if (seen.has(value)) {
        return value;
    return JSON.stringify(obj, getCircularReplacer(), 4);

Then, create a file to dump the data. In the example below, I have 2 files.

  • dump-posts.njk (to dump WordPress post data)
  • dump-pages.njk (to dump WordPress page data)
permalink: dump-post-data.json
{{ readyPosts.all | dump | safe }}
permalink: dump-page-data.json
{{ pages | dump | safe }}

Now, when 11ty builds the site, two pages are created at the following URLs.

  • http://localhost:8080/dump-page-data.json
  • http://localhost:8080/dump-page-data.json

Strong, Waterproof Glue: Liquid Nails Fuze-It Max VS Loctite PL Marine

I wanted to find a glue that was simple to use, not too expensive, was very strong, and would maintain its strength in wet conditions. There are many glue options to choose from. I ended up choosing two popular brands: Liquid Nails and Loctite. For each brand, I chose either the strongest option or the option that was designed for wet environments. I didn’t include epoxy in my test because I didn’t want to mix two parts together and apply the mixture using a stick. Below are the two options I chose.

I first glued a piece of 2×4 to a concrete landscape block.

After waiting 1.5 to 2 days to fully cure, I attached each piece of wood to a chain to suspend the wood / concrete block combo in the air.

After 2 days, both adhesives kept the wood attached to the concrete block. I then wanted to see if water would affect the bond. I submerged each test in water without having it touch the bottom of the bucket of water.

LeftL Fuze-it | Right: PL Marine

After 12 hours, the wood pieces were still connected to the concrete blocks. But, after 23 hours, the Liquid Nails Fuze-It MAX lost its strength and the wood piece became disconnected from the concrete block.

Left: Fuze-It MAX, Right: PL Marine

So, the winner is Loctite PL Marine, although if you are gluing something in a dry environment, Liquid Nails Fuze-It MAX is probably sufficient. Unsurprisingly, the Marine adhesive is stronger both in dry and wet environments, including while being completely submerged in water.

Multiple 4K Monitors for Efficient Multitasking and Increased Work Productivity

As a web developer, I absolutely need multiple windows open – maybe more than for people in other professions. Due to the pandemic, like many people, I had to create a home office. I could have used the unused loft upstairs but I didn’t know I’d still be working from home after 2 years and I like being close to the kitchen so I converted the dining table into my work desk. Since I had some old monitors lying around, I used them to have multiple windows open. I thought we’d all just work from home for a month or two max so it didn’t seem worth it to spend money on fancy monitors.

April 2020

As the pandemic progressed, I got a work allowance to buy office equipment for work from home (WFH). I badly needed an ergonomic and comfy work chair. I also got an ultrawide monitor to have even more screen real estate.

This photo does not show the upgraded chair.

I thought about using my 65″ inch LG OLED TV as a monitor but the screen was WAY TOO BIG!

I then read an article about people using 43″ and 50″ TVs with a low input delay as their work monitor and how happy they’ve been. I got the Samsung 50″ ‘The Frame” TV at a 50% open-box discount at BestBuy. As much as it was nice reading code on it, anything on the left and right was uncomfortable to read.

I could have tried the 43″ model of the same TV but it cost more than I wanted to spend. Plus, I wanted more windows open at the same time. Fortunately, BestBuy had some open-box 32″ Samsung UJ59 4K monitors. Regular price: $340. Sale Price: $300. Open box price: $250 and $195 (both looked brand new even though they were open box items). After being tired of a messy dining room / office where I could see the back of my monitors, I decided to rearrange the dining room and get a more suitable desk.

I know what you’re thinking. Why is the laptop halfway off the desk. Since with two 4K monitors, I didn’t think I needed the laptop screen but, how can I resist an extra screen? I’ll get a drawer unit the same height as the desk, put it on the left side of the desk, and put the laptop on it. More importantly: why in the world didn’t I buy two 4K monitors before? This setup makes working so much better I regret not doing this sooner. Now, this setup works for me but you may need to adjust some settings to better suit yours.

Setting up the monitors

Before you go out and buy a bunch of 4K monitors, make sure you laptop / computer supports them. I have a MacBook Pro (2019) and according to Apple, I can run multiple 4K monitors.

My particular Samsung UJ59 monitors don’t have USB-C input – only HDMI – so I have to use a USB-C to HDMI adapter.

Monitor Layout

On MacOS, I chose to have my laptop screen be my main display and my 32″ monitors both be extended displays. You can then drag the squares representing screens to match your physical layout so that when you drag your mouse (I use a trackpad) off the edge of a screen/monitor, the pointer seamlessly appears on the adjacent screen, as you’d expect.

Monitor Resolution

4K screens/TV support a native resolution of 3840 x 2160 pixels. But, you may find that reading code or blog posts difficult when the font is too small. There are two things you can do to address this:

Scale down the resolution

You can have your monitor show a picture at a lower than 4K resolution such that everything appears bigger.

I set my laptop as the main display with a default resolution
The first 4K monitor is set as “Extended display” with a default (4K) resolution
The 2nd 4K monitor is also set as “Extended display” with a default resolution. When I click the “Scaled” option, you can see the default resolution is 3840 x 2160, which is 4K.

Below are lower scaling options.

The scaling option right below 4K is 3360 x 1890 px.
The lowest scaling option is 1920 x 1080 px.

Increase the font size of some or all browser tabs / apps

If you’re in 4K mode and you feel the font is a bit too small to read, you can increase the font size of just that window or browser tab. I use VisualStudio Code as my code editor and I find that increasing the font size makes for a much more pleasant coding experience. Plus, since I have laid out my screen such that VS Code takes up the full height of the screen, I can still see a ton of code without having to scroll a lot.


One survey indicated that slightly more people preferred a single 4K TV as their monitor than people who prefer multiple 4K monitors. This all boils down to your use case. If you don’t need a million windows open like I do, then a single 43″ 4K TV may be better for you (don’t get a 50″ TV – it’s too big if you are sitting 2-3′ from the screen). Otherwise, multiple 4K monitors is definitely the way to go (with some custom setting adjustments to suit your viewing comfort).


The desk in the picture above was from IKEA. It was too wobbly so I replaced it with this Husky one from Home Depot. The side drawer unit is from IKEA.

Generate a Static Website with 11ty (Eleventy)

This post will describe how to create a simple website with 11ty (Eleventy), a NodeJS-based static site generator (SSG). According to this article, 11ty is the 3rd-fastest SSG.

I’ve tried Hugo. It’s definitely fast and easy to install. But, I found that it wasn’t flexible in how it could be used and not as intuitive as Jekyll and 11ty. I’ve also tried Jekyll. It was a bit tricky to install on Windows. It would sometimes get stuck or not detect file changes and therefore not build and reload them in the browser. LiveReload didn’t work, for me at least. 11ty was super easy to install, as you’ll see below. It’s super flexible, customizable, and intuitive to use. And, it supports a bunch of templating engines like HandlebarsJS, Liquid, Nunjucks, EJS, and even plain JavaScript.

Here’s a step-by-step guide to getting started with 11ty to build a simple website from scratch. This tutorial was done on Windows.

Install the latest version of Node JS

Verify Node JS installation

If you’re on Windows, open the Command Prompt and run node -v to check the version of Node installed.

Optionally, install the latest version of PowerShell

If you’re on Windows, you can use the default command prompt. Or, you can use PowerShell. If you don’t have the latest version of PowerShell, you can download it from here. You can also install Windows Terminal.

Instead of the default command prompt (cmd.exe), I’m going to use PowerShell. Open PowerShell and run node -v to check your version of Node installed. If PowerShell doesn’t find Node, then check that Node is in your path.

Open System Properties

Click Environment Variables

Under System variables, click Path.

If there is no path to the nodejs folder, add it as the last item. 

If it exists but comes before the PowerShell path, then move it down below the PowerShell path.

Close and reopen PowerShell and rerun node -v.

Create a website folder

mkdir eleventy-poc
cd eleventy-poc

Create a package.json file

npm init -y

Install eleventy and save it in package.json

At the time of this writing, the latest release version is 1.0.2. However, I will install version 2.0.0 canary because it has new features that I like.

npm install --save-dev @11ty/eleventy@canary

Run eleventy (verify it runs)

npx @11ty/eleventy

Install HTML 5 boilerplate

We’ll use HTML5 Boilerplate as a basis for our website. Go to and download the latest version.

Extract and copy all files to the project folder (eleventy-poc) EXCEPT for package.json and package-lock.json. You don’t want to overwrite the 11ty dependencies that were already saved there.

Create an “src” folder

To keep our source files separate from root-level environment files (package.json, .env, .gitignore, etc), let’s create an src folder to store our website source files and move the following files and folders into it.

  • css
  • img
  • js
  • 404.html
  • browserconfig.xml
  • favicon.ico
  • icon.png
  • index.html
  • humans.txt
  • robots.txt
  • site.webmanifest
  • tile.png
  • tile-wide.png

The docs folder just explains how to use HTML5 boilerplate. You can delete it if you want. I’m going to delete mine.

Create .eleventy.js config file

Create a new file in the root folder called .eleventy.js. Add to it the following code.

const inspect = require("util").inspect;

module.exports = function(eleventyConfig) {
	eleventyConfig.addPassthroughCopy("src", {
		//debug: true,
		filter: [
	// Copy img folder


	// tell 11ty which files to process and which files to copy while maintaining directory structure
	// eleventyConfig.setTemplateFormats(["md","html","njk"]);

	// Run me after the build ends
	eleventyConfig.on('eleventy.after', async () => {



	// Values can be static:
	eleventyConfig.addGlobalData("myStatic", "static");
	// functions:
	eleventyConfig.addGlobalData("myFunction", () => new Date());

	eleventyConfig.addFilter("debug", (content) => `<pre>${inspect(content)}</pre>`);

	// add support for blocks
    eleventyConfig.addShortcode('renderlayoutblock', function(name) {
        var blockContent = '';
        if ( &&[name]) {
            blockContent =[name];
        return blockContent;
    eleventyConfig.addPairedShortcode('layoutblock', function(content, name) {
        if (! {
   = {};
        }[name] = content;
        return '';

	return {
		dir: {
			input: "src",
			output: "www",
			// ⚠️ These values are both relative to your input directory.
			includes: "_includes",
			layouts: "_layouts",

From top to bottom, the code above does the following

  • includes the util package to help with debugging
  • recursively looks for 404.html, *.css, *.json and *.js files (except for *.11ty.js and *.11tydata.js files) and copies them to the output (build) folder while maintaining folder structure. This allows you to have global JS and CSS files in root web folder, e.g. /js/main.js and /css/main.css, in addition to local, page-specific JS and CSS files, e.g. /company/index.html, /company/index.js, /company/index.css.
  • copies all files from /src/img to the output (build) folder
  • Using setServerPassthroughCopyBehavior, tells 11ty to pass-through copy files both when running a single build (npx @11ty/eleventy) and when building files during development using the --serve flag (npx @11ty/eleventy --serve)
  • tells 11ty to ignore the 404.html file. That file does not need to be built. If it gets built, it will be at /404/index.html, which is not what we want.
  • adds code to show debug information nicely in a web page
  • adds support for template blocks that works with 11ty’s template inheritance system (see note below)
  • specifies the input (source) folder as src, output (build) folder as www, the layouts folder as _layouts and the includes folder as _includes.

Include files are reusable code snippets like components, e.g. header, footer, etc.

Layout files are files that determine a page’s layout. Layout files can wrap other layout files.

Note: Both Nunjucks and 11ty have their own template inheritance mechanism. With Nunjucks, you inherit a parent template using {% extends "parent.njk" %}. With 11ty, you inherit a parent template in the front matter, e.g.
layout: parent.njk

Nunjucks actually supports template “block”s natively, but it doesn’t support front matter. Since there are benefits to having front-matter support, this post will use 11ty’s template inheritance mechanism. In doing so, Nunjuck’s native “block” support doesn’t work as expected. As a workaround, I have added shortcodes in the .eleventy.js config code above to produce the same effect of Nunjuck’s native “block”s. As you’ll see later, we’ll use the shortcodes {% renderlayoutblock %} and {% layoutblock %} instead of the Nunjucks {% block %}.

Re-run Eleventy when you save

To build the pages in the src folder, we can run the following command.

npx @11ty/eleventy

However, during development, it’s handy to have 11ty build any source file changes automatically. We do that using the following command.

npx @11ty/eleventy --serve

This runs a local web server that auto-reloads the browser when you save modified file. 

An output (build) folder (in this case, www) will be created containing the built and copied files.

We can browse to http://localhost:8080/ and see the home page.

Create a default layout

At this point, we have our boilerplate home page (index.html) file.

Let’s create a header, footer, and sidebar containing nav links, and a main content area, like most websites have.

    <p class="logo">ABC Company</p>
  <div class="content-wrapper">
          <li><a href="/">Home</a></li>
          <li><a href="/about/">About</a></li>
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, velit provident sed cum repellendus repudiandae ut mollitia animi. Voluptates expedita corporis pariatur sed quam. Ducimus fugiat quos eum aut eaque?</p>
    Copyright 2022

I’ll also add some basic CSS to main.css to style the page.

/* ==========================================================================
   Author's custom styles
   ========================================================================== */
footer {
    padding: 1em;
    background-color: #70a9dc;
.logo {
  font-size: 2em;
.content-wrapper {
  display: flex;
  min-height: 50vh;
main {
  padding: 1em;
nav {
  background-color: #efefef;
  padding-right: 3em;

Now, 11ty will auto-reload the page and we’ll see

Let’s create a layout from this index.html page and create “include” files containing code snippets that will be included on all pages.

  • Copy index.html to src/_layouts/default.njk (this will be a Nunjucks template)

In src/_layouts/default.njk

  • Prefix all relative paths with “/”, e.g.
    • css/normalize.css becomes /css/normalize.css
    • js/main.js becomes /js/main.js
  • Copy the <head> code block to src/_includes/head.html
  • Copy the <header> code block to src/_includes/header.html
  • Copy the <nav> code block to src/_includes/nav.html
  • Copy the <footer> code block to src/_includes/footer.html
  • Copy all of the <script> tags to src/_includes/scripts.html
  • Replace the <head> code block with {% include "head.html" %} to include the head code
  • Replace the <header> code block with {% include "header.html" %} to include the header code
  • Replace the <nav> code block with {% include "nav.html" %} to include the nav code
  • Replace the <footer> code block with {% include "footer.html" %} to include the footer code
  • Replace the <script> code with {% include "script.html" %} to include the scripts
  • Replace the lorem ipsum text with {{ content | safe }}. This content variable will be replaced with content from pages that use this template, like the home page.

This is how the files should look.

The contents of default.njk should look like this.

<!doctype html>
<html class="no-js" lang="">

{% include "head.html" %}

  {% include "header.html" %}
  <div class="content-wrapper">
    {% include "nav.html" %}
        {{ content | safe }}
  {% include "footer.html" %}
  {% include "scripts.html" %}


Replace the contents of index.html with the following.

layout: default.njk
<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Facilis, voluptates distinctio fuga culpa praesentium quis repellat, mollitia vel repudiandae suscipit officiis tempora atque ratione quidem quo facere reiciendis, iste sit!</p>

The first section between the triple dashes is the front matter in YAML format. Here, we tell 11ty to use the default.njk layout for this page. 

The content after the front matter will go where the content variable
{{ content | safe }} is in the layout.

If we look at the home page in a browser, we’ll see that it looks exactly the same as before, except we’ve templatized the home page using a layout and many “include” files. 

Add template blocks

Template blocks are handy because they let you add content to a parent template from a child template. For example, our base template (default.njk) may contain global content that applies to all pages, e.g. loading main.css, main.js, etc. A child template for a specific page, e.g. /index.njk or /about/index.njk, can use blocks to add local resources like local CSS and JS files specific to those pages.

First, let’s update our default.njk layout to use blocks using custom shortcodes explained above as follows:

<!doctype html>
<html class="no-js" lang="">

  {% renderlayoutblock 'prependMeta' %}
  {% include "meta.html" %}
  {% renderlayoutblock 'appendMeta' %}

  {% renderlayoutblock 'prependStyles' %}
  {% include "styles.html" %}
  {% renderlayoutblock 'appendStyles' %}

  {% renderlayoutblock 'scriptsInHead' %}

  {% include "header.html" %}
  <div class="content-wrapper">
    {% include "nav.html" %}
        {{ content | safe }}
  {% include "footer.html" %}
  {% renderlayoutblock 'prependScripts' %}
  {% include "scripts.html" %}
  {% renderlayoutblock 'appendScripts' %}


Previously, we had a single head section (head.html) in the _includes folder. It’s better to split this up so that individual pages can have page-specific meta tags, styles, and scripts that need to go in the head. Replace head.html with meta.html and styles.html.

Note in the code above how there is a prependStyles and appendStyles block. This allows a child template to add stylesheets before and after the global styles that are included in styles.html. The same applies to the meta and scripts blocks.

Now, update index.html as follows and change the extension from .html to .njk so 11ty can process the blocks.

title: Home
layout: default.njk

{# add any meta tags that you want to come BEFORE other meta tags #}
{% layoutblock 'prependMeta' %}
    {# <meta name="a" content="b"> #}
{% endlayoutblock %}

{# add any meta tags that you want to come AFTER other meta tags #}
{% layoutblock 'appendMeta' %}
    {# <meta name="c" content="d"> #}
{% endlayoutblock %}

{# add any stylesheets that need to come BEFORE other stylesheets #}
{% layoutblock 'prependStyles' %}
    {# <link rel="stylesheet" href="/index.css"> #}
{% endlayoutblock %}

{# add any stylesheets that need to come AFTER other stylesheets #}
{% layoutblock 'appendStyles' %}
    {# <link rel="stylesheet" href="/index.css"> #}
{% endlayoutblock %}

{# add any scripts that need to go in the head #}
{% layoutblock 'scriptsInHead' %}
    {# <script src="/js/x.js"></script> #}
{% endlayoutblock %}

<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Facilis, voluptates distinctio fuga culpa praesentium quis repellat, mollitia vel repudiandae suscipit officiis tempora atque ratione quidem quo facere reiciendis, iste sit!</p>

{# add any scripts that need to go BEFORE other scripts before the closing </body> tag #}
{% layoutblock 'prependScripts' %}
    {# <script src="/js/x.js"></script> #}
{% endlayoutblock %}

{# add any scripts that need to go AFTER other scripts before the closing </body> tag #}
{% layoutblock 'appendScripts' %}
    {# <script src="/js/x.js"></script> #}
{% endlayoutblock %}

In the code above, I added some meta, link, and script tags. They’re commented out using {% ... %} since they are just to demonstrate usage.

Create another page

Let’s create an About page. Create a file at /about/index.html. The content of the file can be

layout: default.njk
Title: About
<h1>{{ title }} </h1>
<p>This is the about page.</p>

In this case, we added a front matter variable called title and output it using curly bracket notation. 11ty is processing these files as Liquid files using the Liquid templating engine.

If we go to http://localhost:8080/about/, we see:

You can also rename about.html to about.njk to use the Nunjucks templating engine. 11ty will process the file accordingly.

11ty supports many templating languages including HTML, Markdown, JavaScript, Nunjucks, Liquid, Handlebars, Moustache, EJS (Embedded JavaScript), HAML, Pug, and custom. I prefer Nunjucks.

Add files for git to ignore

There are some files/folders we don’t want git to track. The HTML5 boilerplate already includes a .gitignore file. Edit it and add “www” to the list. We don’t want to commit the built files to git. Files will be built during deployment. In the .eleventy.js config file, you can also make your build folder “dist” instead of “www”, if you prefer. “dist” stands for “distributable”. Some other common folder names are “public” and “build”,

# Include your project-specific ignores in this file
# Read about how to use .gitignore:
# Useful .gitignore templates:

Draft mode

If you are working on a page that is not ready for production, you can tell 11ty not to build it. Simply add permalink: false in the front matter.

layout: default.njk
title: Draft Mode
permalink: false
<h2>{{ title }}</h2>
<p>lorem ipsum...

Layout chaining

11ty supports template inheritance so that children templates or layouts can inherit their parent templates or layouts. For example, we can create a new layout called sublayout.njk. We indicate that it inherits from another layout (in this case, default.njk) by adding layout: default.njk to the front matter.

layout: default.njk
variable14: value14

<div class="foo">
  <p>This content is wrapped in "div" tag with class="foo".</p>
  {{ content | safe }}

We can then create a new page template that uses this child layout by adding layout: sublayout.njk in the front matter.

layout: sublayout.njk
title: Test nested layout

<h2>{{ title }}</h2>
<p>lorem ipsum...</p>


A template in 11ty has access to data from multiple sources. 11ty merges all data sources. Data with the same keys are overwritten based on priority.

.eleventy.js                      lowest prio (7):  data via addGlobalData method
  site.json                        lower prio (6):  global data file
  default.njk                       high prio (3):  data in layout front matter
  blog.json                          low prio (5b): data in parent dir data file
    posts.json                       low prio (5a): data in directory data file
    article.njk                   higher prio (2):  data in template front matter
    article.json                  medium prio (4):  data in template data file
    article.11tydata.js          highest prio (1):  eleventyComputed in template js

Global data files

Global data is accessible to all templates. You can have global data in /src/_data. The _data folder is a special 11ty global data folder. This folder may contain files like data.json and data.js. Here’s an example data.json file.

    "variable13": "value13"

Here’s an example data.js file.

module.exports = function() {
    return {
        "variable11": "value11"

You can also add global data directly to .eleventy.js using the addGlobalData method, e.g.

eleventyConfig.addGlobalData("myStatic", "static");

Data in layout front matter

In your base/default layout, e.g. default.njk, you can add data to the front matter. This data can then be used by all page templates that inherit from that layout. Here’s an example.

variable1: a
variable2: value2

<!doctype html>
<html class="no-js" lang="">

Local data files

You can have data that is accessible to only specific pages. For example, if you have a page template at /foo/bar/index.njk, then you can have sibling data files at /foo/bar/colors.json or /foo/bar/bar.11tydata.js or /foo/bar/bar.11ty.js. The json data returned from those data files would then only be accessible to /foo/bar/bar.njk.

IMPORTANT: To access local json and data.js data, the files must have the name of their folder, e.g.

  • /foo/bar/bar.json
  • /foo/bar/bar11tydata.js
  • /foo/bar/bar.njk

If your template file is index.njk, it won’t be able to access the data.

Local data in front matter of page template

If you have a page template at /foo/bar/index.njk, you can add data to its front matter. This data would only be accessible to that page template and no other.

Dump and view all global and local data

Inevitably, you will be working on a page that doesn’t output the data you expect. You can enable debug mode, but the debug information may be lost or truncated in your console output. Earlier in this post, we added a filter to .eleventy.js that uses the util node package for debugging. Now, we can use that filter to dump all global and local data to a page in the browser for easy viewing. To do that, add the following code to any page template.

Note: the front matter format has to be JS, not YAML. Also, you must specify the layout in the front matter. Some people use Nunjucks’ {% extends %} but that won’t work for dumping variables.

  layout: "default.njk",
  variable1: "value1",
  eleventyComputed: {
    datum(data) {
      return data;

<h2>Dump of all data</h2>
<pre><code>{{ datum | debug }}</code></pre>

When you view the page in a browser, you’ll see a nice JSON dump of all data like this.

Dump of all data
<pre><ref *1> {
  data: { variable12: 'value12', variable11: 'value11' },
  foo: { bar: { variable13: 'value13' } },
  myStatic: 'static',
  myFunction: 2022-12-29T23:35:04.425Z,
  eleventy: {
    version: '2.0.0',
    generator: 'Eleventy v2.0.0',
    env: {
      source: 'cli',
      config: 'C:/Users/abdul/OneDrive/Documents/Websites/eleventy-poc/.eleventy.js',
      root: 'C:/Users/abdul/OneDrive/Documents/Websites/eleventy-poc',
      isServerless: false
  pkg: {
    name: 'eleventy-poc',
    version: '1.0.0',
    description: '',
    main: 'index.js',
    scripts: { test: 'echo "Error: no test specified" && exit 1' },
    keywords: [],
    author: '',
    license: 'ISC',
    devDependencies: { '@11ty/eleventy': '^2.0.0-canary.23' }
  variable1: 'value1',
  variable2: 'value2',
  variable15: 'value15',
  variable3: 'value3',
  variable4: { variable5: 'value5', variable6: 'value6' },
  variable7: [ { variable8: 'value8', variable9: 'value9' } ],
  variable10: 'value10',
  layout: 'default.njk',
  eleventyComputed: { datum: [Function: datum] },
  page: {
    date: 2022-12-28T03:02:27.808Z,
    inputPath: './src/foo/bar/bar.njk',
    fileSlug: 'bar',
    filePathStem: '/foo/bar/bar',
    outputFileExtension: 'html',
    templateSyntax: 'njk',
    url: '/foo/bar/',
    outputPath: 'www/foo/bar/index.html'
  collections: {
    all: [ [Object], [Object], [Object], [Object], [Object], [Object] ]
  datum: [Circular *1]

Read my other post about other ways to debug 11ty pages.

Components / Macros / Functions

Inevitably, you will come across a situation where you want to have a slightly different version of an HTML snippet. For example, you can have a snippet of HTML for a hero section and you want to have the option of showing different hero images. Instead of creating a bunch of include/partial files containing almost identical code, you can create components that accept parameters. These components behave like functions. In Nunjucks, they are called macros.

1. Create a component loader

First, we’ll create a Nunjucks’ macro that will simplify how we load other macros. I’ll put it at /includes/component.njk.

{%- macro component(name, params) -%}
  {%- include name + ".njk" ignore missing -%}
{%- endmacro -%}

2. Create some components

For demonstration purposes, I’ll create two simple components. The first one will show a button with a customizable label. I put it at /includes/button.njk.

<button type="button">{{ params.primary }} {{ params.secondary }}</button>a

The second one just shows a message in a box. If a certain parameter is present, then an additional second message will be shown. I put this at /includes/note.njk.

<div style="background-color: #efefef;">
    <p>This is a message</p>
    {% if params.show2ndMsg %}
        <p>This second message only shows if the show2ndMsg variable is true</p>
    {% endif %}

3. Call the components

Now, in a page template where we want to show the component, we just need to call the component loader (component.njk) and the components themselves, optionally passing in some parameters.

title: "Component Macro"
layout: "default.njk"
{%- from "component.njk" import component -%}

  {{ component('button', {
    primary: 'Hello'
  }) }}

  {{ component('button', {
    primary: 'Hello',
    secondary: 'World'
  }) }}


{{ component('note') }}

{{ component('note', {
    show2ndMsg: true
}) }}

When the page is built, this is what we’d see.

Now, let’s save our website code (source only) to GitHub and automatically deploy it to Netlify all for free.

Add the local repo to GitHub

We’ll follow these instructions on how to add our local repo to GitHub.

Create a new repository on

To avoid errors, do not initialize the new repository with README, license, or gitignore files. You can add these files after your project has been pushed to GitHub.

Initialize the local directory as a git repo. Run git init -b main in our project folder. This will create a hidden .git folder.

Add the files in your new local repository. This stages them for the first commit.

git add .

Commit the files that you’ve staged in your local repository.

git commit -m "First commit"

At the top of your repository on’s Quick Setup page, click to copy the remote repository URL.

In the Command prompt, add the URL for the remote repository where your local repository will be pushed.

$ git remote add origin  <REMOTE_URL> 
# Sets the new remote
$ git remote -v
# Verifies the new remote URL

Set tracking information for the main branch

git branch --set-upstream-to=origin/main main

Push the changes in your local repository to

git push origin main

Go to your repo on GitHub and see your files there.

Host site on Netlify

Now, let’s host our site for free on Netlify. Create a Netlify account and add a new site by importing an existing project.

Connect Netlify to GitHub and choose a repo. If you don’t see your repo, configure Netlify to be able to access some or all repos in your GitHub account.

Netlify will analyze your site and suggest default build settings. As you can see below, Netlify correctly detected that my site was built using 11ty so it populated the “Build command” and “Publish directory” using the default Eleventy values.

Click “Deploy site’ and wait a minute or two for your site to be deployed. Netlify will clone the repo, install any dependencies as listed in the package-lock.json file, run the Eleventy build command, and host the built files. You will then see how long it took to build the entire site and be provided a free Netlify URL where you can view the live site. In this case, the entire build took 37s.

The free Netlify URL generated is

Going to that URL, we see the same site as we saw on our local dev machine.

Let’s change that subdomain to someone simpler and more relevant. In Netlify, go to Settings > General > Site Details and change it. I’ll change mine to “my11tywebsite”. Now, I can visit my website at

Add a Custom Domain

To add a custom domain that you’ve registered at a domain registrar other than Netlify, e.g. GoDaddy, you’ll need to delegate the domain to Netlify and update the nameservers to point to Netlify’s nameservers.

First, click on “Domains” > “Add or register domain”.

Enter your custom domain and click “Verify”.

You will then see Netlify’s nameservers. You’ll need to go to your domain’s registrar and update the nameservers to these servers.

Once you’ve done that, you need to associate the domain with a particular site. Go to Site Settings > Domain Management > Domains and click Add Custom Domain. Enter the domain you just delegated to Netlify and click Verify.

Add SSL/TLS Certificate

You can now add SSL/TLS to your site so you can access it at the HTTPS protocol. Go to Site Settings > Domain Management > Domains and click HTTPS. Then click Provision certificate.

Environment Variables

Environment variables are useful for storing settings and secret keys for different environments, e.g. development, staging, and production. First, install the dotenv node package.

npm install dotenv

Then, create a .env file in the root of your project (as a sibling of package.json and .gitignore) and add some variables to it.

# ADD .env TO .gitignore


Since I’m on my development machine, I set the ENVIRONMENT variable to “development”.

Since the .env file contains passwords and secret keys, and it only applies to a specific environment, don’t commit it. Add it to your .gitignore file, e.g.

In order to access the environment variables in 11ty page templates, we need to load them as a global variable in our global JS file. Edit _data/site.js as follows. Here


module.exports = function() {
    return {
        "environment": process.env.ENVIRONMENT,
        "db_host": process.env.DB_HOST,
        "db_user": process.env.DB_USER,
        "db_password": process.env.DB_PASSWORD

Since the file is called site.js, the global variables will be accessible under the “site” object, e.g. site.environment, site.db_host, etc. If you dump all data to a page, as explained above using the eleventyComputed front matter, you should see these environment variables. Here’s an example page template demonstrating how to use environment variables in a page template.

title: "Test Environment Variables"
layout: "default.njk"


site.environment = {{ site.environment }}

{% if site.environment == "production" %}
    <p>I'm running in production</p>
{% elseif site.environment == "staging" %}
    <p>I'm running in staging</p>
{% elseif site.environment == "development" %}
    <p>I'm running in development</p>
{% endif %}

On your local development machine, you’ll see this.

Since our production website is running on Netlify, we can add production environment variables in Netlify. Under Site settings > Environment variables, we can add environment variables.

And as expected, the page in production shows different (production) values.

Staging Site

At this point, we have two environments:

  • development (localhost:8080)
  • production (Netlify domain:

When working with a larger team, you’ll often need to show people a preview of some changes for their review. This is normally done on a staging site. To create one, we’ll create a branch in Github called “staging” and tell Netlify to deploy it to staging– In Github, create a branch as shown below.

Now, we should see 2 branches and be able to switch between the two. At this point, both branches are in sync with each other and contain the same files.

In Netlify, go to Site > Settings > Build & Deploy > Continuous Deployment > Branches and add the staging branch.

To trigger a build and deployment on staging, let’s make a simple change to the readme file and commit it. Netlify will detect the commit to the staging branch in Github, build the site from that branch, and deploy it to https://staging–

Now, we don’t want the public to be able to view the staging site, so let’s password-protect it. Go to Site Settings > Access Control > Visitor Access and set a global password. This is a paid feature.

Publish changes from staging to production

Once the staged changes are reviewed and approved, we can publish them by merging the changes from the staging branch to the production branch. You’ll see in Github a notice that the staging branch is ahead of the main branch by 1 commit.

You can use git on the command line or a git client, e.g. Github Desktop, to perform the merge. You can also do it in Github. Click the green Compare & pull request button. This will allow you to open a pull request with the repo owner. The repo owner can then accept the pull request to merge in the changes.

In Github Desktop, switch to the main branch and then choose the staging branch to merge into main.

Click the Create a merge commit button. You’ll then need to push the commit to the local main branch to the remote main branch on Github. In Github Desktop, you can do that by clicking the Push origin button.

Now, both branches are in sync and Netlify will trigger a build and deploy of the changes from the main branch.

Reverting the website to a previous version

Netlify uses atomic deploys to deploy versions of a website to its many nodes in its CDN. Unlike deploying files one by one, atomic deploys ensure a globally consistent version of the website is deployed. You can think of it as zipping up the whole site, uploading the zip file, then unzipping the file to deploy the site. If the unzip process fails, then entire deployment fails. Users will not see a partially-deployed (and broken) website. One of the benefits of this is the ability to instantly roll back a mistake directly in Netlify because Netlify keeps a history of deployments. Rather than reverting a change in git and committing it to fix the mistake, you can simply click on a previous deployment and then click the “Publish Deploy” button. You can then take your time to fix your code in git. Learn more.

Preview past deployments

If you’d like to see how a previous version of your website looked, you can preview them rather than roll back to them. Each deployment is atomic and Netlify provides a publicly accessible live version of each deployment by prefixing a unique hash to the website domain. For example, I made two simple changes to the home page and committed and deployed each change separately.

The most recent deployment, which is labeled “Published” and has commit message “Commit DEF”, is accessible at the production URL:

Notice the subheading says “DEF”.

This deployment is also available at

The previous deployment, which has commit message “Commit ABC”, is available at the permalink: https://63b9faa6b19a020008be537f–

Notice the subheading says “ABC”.

Fetch and cache network resources

You’ll often find yourself in need of pulling data from an API endpoint that returns JSON data and displaying that data in a page. This can easily be done using the eleventy-fetch plugin. Let’s test this out. First, install the eleventy-fetch plugin.

npm install @11ty/eleventy-fetch

We’ll need a URL where the JSON data is located. For testing purposes, I created a JSON file at the root of my project in GitHub so I can access it at

The file just contains the following JSON data.

    "colors": [
        {"id": 1, "color": "Red"},
        {"id": 2, "color": "Green"},
        {"id": 3, "color": "Blue"},
        {"id": 4, "color": "Purple"}

Now, let’s add an 11tydata.js file to fetch this data with the following code.

const EleventyFetch = require("@11ty/eleventy-fetch");

module.exports = async function() {
	try {
        let url = '';

		/* This returns a promise */
		return EleventyFetch(url, {
			duration: "1m",
			type: "json"
	} catch(e) {
		return {
			// my failure fallback data

I set a cache duration of 1 minute.To display the data, I created a simple .njk template with the following content:

title: "Test Fetching JSON From Remote URL with Caching",
layout: "default.njk",

{% for color in colors %}
    <li>ID: {{}} - Color: {{color.color}}</li>
{% endfor %}

If you build the site locally, you will see a .cache folder created with two files.

The eleventy-fetch-07e8b9a55a957158da72b706594705.json file contains the JSON data as shown below.

The eleventy-fetch-07e8b9a55a957158da72b706594705 file contains information about the filename of the data file and when it was cached.

Since the cache duration was set to one minute, if you rebuild the site within one minute, the eleventy-fetch plugin will not fetch the remote data but rather use the cached data. However, if you rebuild the site after one minute, the eleventy-cache plugin will fetch the remote data and update the cache and the web page.

The above works fine locally. However, remotely, Netlify doesn’t cache data by default. As a result, Netlify will always fetch the remote data. In order to get the same behavior as on Netlify, we need to persist the .cache folder. This can be done by installing the netlify-plugin-cache plugin and creating some directives in a .toml configuration file. First, install the plugin:

npm install netlify-plugin-cache

Also, verify that .cache is listed in your .gitignoore file like this.

Do not ever commit the .cache folder to version control. Make sure .cache is listed in your .gitignore file.

Commit these files to GitHub so they can trigger a Netlify build. Once Netlify is done building the page, it will save the cached files remotely. This way, you can cut down on API requests and build pages faster. You can adjust the cache duration time in the 11data.js file. Learn more about 11ty fetch.


You can add redirects in two ways: in a _redirects file and in a .toml file. We’ll add redirects to a _redirects file. First, create a file called _redirects in the src folder with the following contents

# Redirects from what the browser requests to what we serve
# 301: page has permanently moved to a new location
# 302: page has temporarily moved to a new location
/home               /               301
/foo                /about          302

This file has to be in the src folder because it needs to end up in the build output (www) folder. To have 11ty copy the _redirects file to the output folder, add it to the eleventy.js config file.

eleventyConfig.addPassthroughCopy("src", {
		//debug: true,
		filter: [

You can’t test redirects locally during development. They only work in Netlify.

Commit your _redirects and eleventy.js files and push them to Github. Netlify will trigger a build and in the deploy summary, you’ll see how many redirects were processed, e.g.

You can test the redirects on Netlify. They work.

Learn more about adding redirects, including the use of wildcards.

Hosting Large Binary Files

By default, git handles binary files (images, PDFs, etc) differently than text files. With text files, only the text changes are saved with each commit. With binary files, the entire file is saved with each commit. As a result, your git repo can quickly become huge. Since Netlify clones your Github repo with every build, this not only causes build times to take longer, it also will increase your Netlify bill. You can use Git LFS and Netlify Large Media, but if you don’t need to version binary files, why bother. You might as well store binary files on Amazon S3/CloudFront which is dirt cheap. If you don’t like the cryptic AWS CloudFront-provided domain, you can add your own domain, e.g. In your website code, you can reference images, for example, using the CloudFront-hosted URL.

Optimizing Images

As mentioned above, hosting binary files that don’t need to be versioned can be done in AWS S3/CloudFront. But, AWS can’t optimize images automatically. Therefore, you should using a dedication image optimization service like ImageKit or Cloudinary. These services can clone your images from your origin, e.g. CloudFront, and host them on their servers while also optimizing them.


With the setup above, your workflow could be like this:

  1. Upload binary files (images, pdfs, etc), if any, to AWS S3/CloudFront. S3 supports uploading multiple files and maintains folder structure.
  2. Edit HTML, CSS, JS locally
    1. If there are images, reference the ImageKit image CDN URL
    2. If there are links to other binary files, e.g. pdfs, link to the AWS CloudFront URL. For PDFs, optimize them for web in Adobe Acrobat (Tools > Optimize).
    3. Test at http://localhost:8080
  3. Commit to staging branch and push to GitHub
  4. Preview changes at on Netlify (this site should be password protected)
  5. Share staging URL with others for review and approval
  6. When ready to publish, merge changes from staging branch to master branch, commit, and push to Github
  7. Verify changes at on Netlify

Set Up a Jamstack Develop, Build, and Release System Using GitHub, Hugo, and Netlify

Jamstack is the new standard architecture for the web. Using Git workflows and modern build tools, pre-rendered content is served to a CDN and made dynamic through APIs and serverless functions. Technologies in the stack include JavaScript frameworks, Static Site Generators, Headless CMSs, and CDNs.

Traditional Web (left) vs Jamstack (right)

Jamstack benefits

Jamstack offers many benefits over traditional web stack.

  • Security
    Published text files are HTML, CSS and JavaScript only. No server-side scripts like PHP
  • Scalability
    As website traffic grows, scaling is much easier when your website amounts to a bunch of static HTML files that can be served from a CDN.
  • Performance
    Static HTML files don’t require server-side processing. Files can be served from a CDN.
  • Maintainability
    It’s much easier to maintain a simple server that hosts static files rather than application and database servers. The heavy lifting of building the static files is done before deployment resulting in stable production files.
  • Portability
    Since Jamstack sites are pre-generated, the production files are simple static files which can be hosted anywhere on any simple host.
  • Developer experience
    Jamstack sites are built on widely available tools and conventions making it easier for developers to learn and develop.

Learn more

Jamstack is a term that was coined by two developers who pioneered the architecture while working at Netlify. This post will explain how to set up a web development, build and release system using the following components

  • Hugo (static site generator)
  • GitHub (version control)
  • Netlify (CI/CD + serverless web hosting)
  • Netlify CMS (content management system)
  • (image optimization and CDN)

Web development workflow

Let’s say you have a website at This workflow will depend on having a GitHub repo with 2 branches (main and develop) and 2 sites hosted on Netlify, one for production ( and one for staging ( The staging site will have site-wide password protection using an option in Netlify.

The workflow using this setup is very common:

  1. Edit web page files locally, e.g. using VisualStudio Code
  2. Commit and push changes to develop branch in private GitHub repo
  3. Any commit to the develop branch in GitHub auto-triggers a build to generate and deploy static files to a staging site in Netlify (
  4. Preview changes in a password-protected, external staging URL
  5. Merge and push changes from develop branch to main branch. This will auto-trigger a build to generate and deploy static files to a production site in Netlify
  6. View published changes in production (


  • If you need to make an edit, you can make it directly in GitHub as well.
  • If you’d like to provide a CMS for your pages (usually for non-technical people), you can use Netlify CMS.

Create GitHub repository and branches

Let’s first create a GitHub repo. We’ll call it and make it private since we don’t want competitors seeing our potentially confidential information. Within this repo, we’ll have 2 branches.

  • main (for / production)
  • develop (for / development)

GitHub’s default branch is called main so we’ll use that name for the production branch.

Install Hugo

Since I’m writing this post on a Windows 11 laptop, I will follow the instructions here. For other OSs, follow the instructions here.

  1. Create some folders (C:\Hugo\Sites and C:\Hugo\bin)
  2. Download the Windows executable from the Hugo Releases page (in my case, I’ll go with the extended version to make sure I have all the features I may need –
  3. Extract the executable to C:\Hugo\bin. You will end up with 3 files like this.
  1. Add the executable to the PATH
    1. open Windows PowerShell
    2. change to the C:\Hugo\bin folder
    3. append C:\Hugo\bin to the existing PATH by entering
      $Env:PATH += ";C:\Hugo\bin"
  1. Verify the update by outputting the PATH value using the command
    Write-Output $Env:PATH
  1. Verify the executable by running the command hugo help. You should see output like below.

Create a new site

Now that Hugo is installed, we can use it to create a new site. Run the following commands.

cd C:\Hugo\Sites
hugo new site

You should then see output indicating a new site was created.

In the Hugo/Sites folder, you should see files and folders as shown below.

Here’s what the folders are for.

├── archetypes (templates for different content types)
├── config.toml (top level configuration file)
├── content (this is where content goes in HTML or markdown format)
├── data (for local and remote dynamic content, e.g. JSON data)
├── layouts (layouts for pages (list pages, home page) and partials, etc)
├── static (images, CSS, JavaScript, etc)
└── themes (for themes)

Install a theme

Now, you can browse themes, install one and make customizations to it. In my case, I will create a theme from scratch.

Create a new theme

Run the command hugo new theme exampleTheme to create a theme called “exampleTheme”.

C:\Hugo\Sites\> hugo new theme exampleTheme
Creating theme at C:\Hugo\Sites\\themes\exampleTheme

This will create an exampleTheme subfolder in the themes folder. The folder structure should look like this:

├── archetypes
│   └──
├── config.toml
├── content
├── data
├── layouts
├── resources
│   └── _gen
│       ├── assets
│       └── images
├── static
└── themes
    └── exampleTheme
        ├── LICENSE
        ├── archetypes
        │   └──
        ├── layouts
        │   ├── 404.html
        │   ├── _default
        │   │   ├── baseof.html
        │   │   ├── list.html (for the list page / index of blog posts)
        │   │   └── single.html
        │   ├── index.html (home page)
        │   └── partials
        │       ├── footer.html
        │       ├── head.html
        │       └── header.html
        ├── static
        │   ├── css
        │   └── js
        └── theme.toml

Note the following files:

  • themes/exampleTheme/layouts/index.html
    this is the home page layout
  • themes/exampleTheme/layouts/single.html
    this is the layout for a single page type, e.g. blog post
  • themes/exampleTheme/layouts/list.html
    this is the layout for a list of page types, e.g. blog posts

When you start development, you want to run hugo server to have Hugo detect file changes and reload your local browser for you. In the output below, we see that the local server is at http://localhost:1313. If I go to that URL, I will see a blank screen since I haven’t created any web pages yet.

Notice also in the output that there are some warnings because we’re missing some files. They will be fixed.

Edit config file

I’m using VisualStudio Code to edit files. First, I’ll update the config.toml file by changing the base URL and title. In order to use the new theme we created, we need to reference it in the config file by adding theme = 'exampleTheme' to it.

Most websites have a main menu for navigating from one page to another. The menu links are common to all pages. Let’s define them in the config file by adding the following to it.

    name = "Home"
    url = "/"
    weight = 1
    name = "Posts"
    url = "/posts/"
    weight = 2
    name = "Tags"
    url = "/tags/"
    weight = 3

Layouts & partials

If you open the theme’s layouts folder, you’ll see some default layouts and some partials layouts. The base layout (baseof.html) is the parent-most layout. As you can see in the screenshot below, the base layout has

  • some partials
  • a main block

The list (list.html) and single (single.html) layouts will use the base (baseof.html) layout. The contents of the list and single layouts will go in the main block of the base layout.

The base layout includes partials for the

  • head section (head.html)
  • header section (header.html)
  • footer section (footer.html)

of every page. Partials are like include files. They are small, context-aware components that can be used economically to keep your templating DRY. These are standard sections of most websites which is why they’ve been auto-generated. If you don’t want to use a particular section, you can remove it from the base layout or leave it as is as those partial files are empty by default.

Now, let’s fill out the layouts and partials.

head.html partial

Open in a text editor. This will be the place for metadata like the document title, character set, styles, scripts, and other meta information. Paste the following

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="">
    <link rel="stylesheet" type="text/css" href="/css/style.css">
    {{ $title := print .Site.Title " | " .Title }}
    {{ if .IsHome }}{{ $title = .Site.Title }}{{ end }}
    <title>{{ $title }}</title>

There are two CSS files. We can create an empty style.css file and put it at

For the page title, we will show the site title (taken from config.toml) if we are on the home page, otherwise, we’ll show the site title followed by the page title of the page we’re on.

header.html partial

Normally in a website’s header you will find navigation links. Open  and paste the following

<div id="nav-border" class="container">
    <nav id="nav" class="nav justify-content-center">
        {{ range .Site.Menus.main }}
        <a class="nav-link" href="{{ .URL }}">
        {{ $text := print .Name | safeHTML }}
        {{ $text }}
        {{ end }}

Note how the navigation link names and URLs are coming from the config.toml file we updated earlier. The keyword range causes the template to loop over the items in .Site.Menus.main array. Alternatively, you could just hardcode the nav links directly in the header.html file.

footer.html partial

For the footer, let’s just add a basic copyright disclaimer.

<p class="footer text-center">Copyright (c) {{ now.Format "2006"}}</p>

The current year will be displayed automatically. If you are wondering why “2006”, you can find out more about it here.

script.html partial

Most websites have some JavaScript. The Hugo auto-generated partial files didn’t include a script partial for JavaScript that should be loaded on all pages. Let’s create one at and paste the following code. Later on, we can add other scripts like jQuery to it.

<script src="" integrity="sha256-0rguYS0qgS6L4qVzANq4kjxPLtvnp5nn2nB5G1lWRv4=" crossorigin="anonymous"></script>
<script src="script.js"></script>

Like style.css, we’ll also need to add a script.js file and put it at

metadata.html partial

Let’s create one more partial to display metadata about each post, e.g. date and tags. Each blog post will have front matter containing key-value pairs specific to each post, e.g.

author: "John Doe"
title: "My First Post"
date: "2006-02-01"
tags: ["foo", "bar"]

The keys can be referenced as variables in our new partial. Create a new file and paste the following code.

{{ $dateTime := .PublishDate.Format "2006-01-02" }}
{{ $dateFormat := .Site.Params.dateFormat | default "Jan 2, 2006" }}

<time datetime="{{ $dateTime }}">{{ .PublishDate.Format $dateFormat }}</time>
{{ with .Params.tags }}
    {{ range . }}
        {{ $href := print (absURL "tags/") (urlize .) }}
        <a class="btn btn-sm btn-outline-dark tag-btn" href="{{ $href }}">{{ . }}</a>
    {{ end }}
{{ end }}

baseof.html layout

As mentioned earlier, the base layout was auto-generated by Hugo. But, since we created a script.html partial for JavaScript files and code that needs to load on all pages, let’s add that partial to the base layout. Open and add script.html after footer.html so that JavaScript does not block page rendering.

<!DOCTYPE html>
    {{- partial "head.html" . -}}
        {{- partial "header.html" . -}}
        <div id="content">
        {{- block "main" . }}{{- end }}
        {{- partial "footer.html" . -}}
        {{- partial "script.html" . -}}

list.html layout

This layout will be used to display a list of blog posts. As explained earlier, the content in this file will define the main block of the baseof.html layout. We’ll create a simple layout for listing blog posts. Open and paste the following code.

{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ range .Pages.ByPublishDate.Reverse }}
    <h3><a class="title" href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
    {{ partial "metadata.html" . }}
    <a class="summary" href="{{ .RelPermalink }}">
        <p>{{ .Summary }}</p>
{{ end }}
{{ end }}

Note how this layout contains a reference to {{ define "main" }} because we are defining the main block of the base layout. We’re referencing .Summary because we only want to show a summary of each blog post.

single.html layout

This layout will be used to display a single page or blog post. As explained earlier, the content in this file will define the main block of the baseof.html layout. We’ll create a simple single-post layout. Open example/themes/exampleTheme/layouts/_default/single.html and copy and paste the code below.

{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ partial "metadata.html" . }}
{{ .Content }}
{{ end }}

Notice how we are including the metadata.html partial in this layout. Unlike in the list partial, which references the Summary variable, this single partial references the Content variable because we want to show the entire blog post content.

Home page layout

The home page layout is at Since we’re loading Bootstrap CSS, we can use the Jumbotron component to render a hero section. Paste the following.

{{ define "main" }}
<div id="home-jumbotron" class="jumbotron text-center">
  <h1 class="title">{{ .Site.Title }}</h1>
{{ end }}

404 layout

Hugo auto-generated an empty 404.html layout in our theme. Since Hugo comes with a default 404, we can delete this one or customize it, if we want.

Write your first blog post

In a terminal, run this command hugo new posts/

When you do that, Hugo creates a file with some default front matter.

The template for creating new page types is in the archetypes folder. By default, there’s only one called It’s a markdown file.

Open the newly created content file at and add some tags to the front matter followed by some content in either Markdown or HTML. Hugo automatically takes the first 70 words of your content as its summary and stores it into the .Summary variable. Instead, you can manually define where the summary ends with a <!--more--> divider. Alternatively, you can add a summary to the front matter if you don’t want your summary to be the beginning of your post.

title: "My First Post"
date: 2020-01-26T23:11:13Z
draft: true
tags: ["foo", "bar"]
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum odio. A erat nam at lectus urna duis. 

Sed velit dignissim sodales ut eu sem. Lectus urna duis convallis convallis 
tellus. Diam sit amet nisl suscipit adipiscing bibendum est. Sed felis eget 
velit aliquet sagittis id consectetur. Vulputate dignissim suspendisse in est ante in nibh mauris cursus. Morbi quis commodo odio aenean. Mollis nunc sed id semper risus in hendrerit gravida rutrum.


Ac ut consequat semper viverra nam. Hac habitasse platea dictumst vestibulum 
rhoncus. Amet porttitor eget dolor morbi non. Justo eget magna fermentum 
iaculis eu non. Id eu nisl nunc mi ipsum faucibus vitae aliquet nec. Aliquam 
id diam maecenas ultricies. Non sodales neque sodales ut etiam. Amet massa 
vitae tortor condimentum lacinia quis. Erat imperdiet sed euismod nisi porta. 

Nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Viverra 
suspendisse potenti nullam ac. Tincidunt id aliquet risus feugiat in. Varius 
quam quisque id diam vel. Egestas erat imperdiet sed euismod nisi. celerisque felis imperdiet proin fermentum leo vel orci porta non. Ut faucibus pulvinar elementum integer. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl.

Viewing the site

Open a terminal and run the following from the root folder of your site

hugo server -D

The -D flag means to include content marked as draft. Alternative, you can change draft: true to draft: false in the front matter. When I ran that command, I got an error in the terminal.

Since I knew Hugo would serve the site at http://localhost:1313, I went there to see what Hugo would show in this case. Fortunately, Hugo also shows a descriptive error in the browser as well.

Apparently, I added a reference to the script.html partial but I forgot to create the partial. After creating the partial, I reran hugo server -D and I get the following output showing no errors.

The output says that the web server is at http://localhost:1313. Navigating there shows the site as shown below.

This is the home page
This is the list view (there’s only one post right now)
This is the single blog post detail view

Auto-Publish a Static Website Using GitHub and Netlify

Let’s say you want to build a simple, static website with the following requirements.

  • You want to keep track of changes to each file using version control.
  • You don’t want to spend a lot of money.
  • You want a simple setup, intuitive interface, and automatic publishing of changes.
  • You want SSL encryption with a certificate that automatically renews.
  • You want your own custom domain.
  • You don’t want to manage servers and you want to focus on your content.

You can accomplish this in many ways. But, I think the easiest way is to use GitHub and Netlify.


GitHub, Inc. is a provider of Internet hosting for software development and version control using Git. It offers the distributed version control and source code management functionality of Git, plus its own features.


Netlify is a San Francisco-based cloud computing company that offers hosting and serverless backend services for web applications and static websites.

This post will show you how easy it is to set up a simple website using GitHub and Netlify.

1. Create a free GitHub and Netlify account

This step is self-explanatory. If you create a GitHub account first, you can then log in to Netlify using your GitHub account, which saves time.

2. Create a repository in GitHub

I created a private repository (repo) called “documentation” to create a hypothetical website containing product documentation. When I created the repo, I opted to automatically create a file to explain what the repo is about. At this point, there’s only one file in the repo.

3. Create a new project in Netlify

When you create a new project, you have a few options. In this case, we’ll choose “Import from Git”,

4. Choose Git provider

Since we’re using GitHub, choose GitHub.

5. Connect Netlify to GitHub

In order to auto-publish from GitHub to Netlify, you need to connect the two.

6. Choose repositories

You may choose to connect Netlify to all of your GitHub repositories or just some. In this example, I just want Netlify connected to the one “documentation” repo I just created.

7. Choose a branch to deploy

In this example, I only have one default branch called “main” in the “documentation” repo. Since I just need to transfer static files in GitHub to Netlify, there is nothing to build.

8. Deploy site

Once you click the “Deploy Site” button, Netlify will take all existing files in your GitHub repo and host them. You can see the status of the deployment in the “Site overview” tab along with the default URL. Notice that in steps 2 and 3 of the screenshot below, you can set up a custom domain and secure your site automatically with a Let’s Encrypt certificate.

9. See the deployed site

If you go to the auto-generated URL, you’ll see a Page Not Found error. That’s because we only have a file in the repo.

10. Create an index.html file

Let’s create a home page for our new website. In GitHub, create a new file with filename “index.html” and add some HTML to it.

Commit the new file. For simplicity, I will leave the default values and click “Commit new file”.

You will then see that you have two files in the repo.

11. View deployment status

When you committed the file in GitHub, that automatically triggered a deployment to Netlify. In Netlify, click the “Deploys” tab to check the status of the deployment. In the screenshot below, we see that the status is “Published”.

12. View the deployed change

Going to the auto-generated URL in Netlify, we see that our new home page has been published.

13. Handling Large Media (Images, PDFs, etc)

If you have large, non-text media such as images and PDFs, to avoid bloating your repo, you can use Git LFS. Clicking on the “Large Media” tab gives you more information about that. Personally, for images, I prefer to use Alternatives are Cloudinary and Uploadcare.

14. Analytics

You can install Google Analytics on your website and/or you can get analytics from Netlify for $9 per month. Clicking on the “Analytics” tab provides more information.


If you need to create redirects, you can use a simple redirect file.

Or, you can create advanced redirects in the Netlify configuration file.

Learn more about redirects.

Access Control

If you want a staging environment where certain people can preview changes before you push them to production, one way to do this is by

  1. creating a “develop” branch in your GitHub repo
  2. connecting Netflify to that branch
  3. enabling site-wide password protection in Netlify (password protection is a paid feature) on the site sourced from that branch.

Though Netlify supports basic authentication, it is not the most secure method of protecting a site. Instead, you can use Netlify’s site-wide protection which uses JWT secret.

Learn more