Video Editing with Capcut

In this post, I’ll share my process for how to make a traditional video in Capcut. Specifically, I’ll show how to make a video that:

  1. comprises a collection of video and image clips
  2. has a simple crossfade (mix, fade-in/fade-out) transition between clips
  3. has background music with a crossfade between music clips
  4. uses AI to stabilize some shaky video footage
  5. improves the color and lighting of some clips
  6. animates some images with a subtle zoom effect
  7. animates some images to create rolling credits

1. Download and install Capcut

Visit the Capcut website, download Capcut and install it.

2. Import video and image footage

  1. Open Capcut
  2. Create a new project
  3. Click the default project name at the top center and rename it to something descriptive, e.g. My Vacation
  4. Click the “Media” tab at the top left and then click “Import” tab below it.
  5. Click the “Import” button or drag your footage (audio, video, images) to the assets pane
  6. Drag some or all of your footage to the timeline at the bottom

Drag the zoom slider at the top right of the timeline pane to zoom the timeline in and out.

3. Trim images

When you drag images to the timeline, the duration will be whatever the setting is under Options, which defaults to 5 seconds. If you want to change the default to, say, 3 seconds, go to Menu > Settings > Edit (tab) > Image Duration and change the value to 3.

Click on an image in the timeline. In the right pane, you will see various options to edit the image.

The playhead is a visual marker that indicates the current position or frame being viewed in the preview pane. Drag the playhead to anywhere above the image in the timeline. You will see a preview of the image in the center in the preview pane.

Click the play button in the preview pane to preview how the image will appear relative to the rest of the video project.

If the image duration is too short or too long, you can extend the duration by dragging the left or right edge of the clip in the timeline to the left or right. You can also position the playhead at a particular time and click one of the following buttons:

  1. SPLIT: to split the image clip into two clips
  2. DELETE LEFT: to delete the portion of the image to the left of the playhead
  3. DELETE RIGHT: to delete the portion of the image to the right of the playhead

4. Trim Videos

Trimming video clips on the timeline is similar to trimming images. Click on a video clip in the timeline. You will see a preview of the video in the preview pane. In the right pane, you will see various options to edit the video clip.

In the right pane, click “Speed” to see the video clip’s duration. If you change the duration value here, the video clip’s speed will change accordingly. If you don’t want to speed up or slow down the video, you’ll need to trim the video clip the same way you trim image clips:

  • drag the left end of the video clip in the timeline to the right
  • drag the right end of the video clip in the timeline to the left
  • split, delete left, or delete right the video clip relative to where the playhead is in the timeline

5. Speed up or slow down video clips

If your video clip is too slow or too fast, you can speed it up or slow it down. As in the previous step, click the video clip in the timeline and then click the “Speed” tab in the right pane.

Speed up

To speed up the video clip, e.g, 2x for twice the speed, just drag the speed marker to the right. The duration of the clip will change accordingly.

Slow down

To slow down the video clip, e.g. 0.5x for half the speed, drag the speed marker to the left. The duration of the clip will change accordingly. Note that if your video clip was shot at 30 frames per second (fps) and you slow down the clip by 50% (0.5x), the resulting video will stutter due to an insufficient number of frames. This is why you should shoot video at a high frame rate, e.g. 60 or 120 fps, if you know you want to play it back in slow motion, which is usually done for action scenes.

If you didn’t shoot at a high fps, you can use Capcut to smoothen the slow-mo effect by either

  • frame blending
  • optical flow

The results are not as good as a high-fps video, but they’re better than without smoothening, especially using the “optical flow” option.

6. Add transitions between clips

There are many transition effects available. I prefer to use the simple crossfade (mix) transition, which gradually fades out one clip and fades in the next clip. To add this transition between two adjacent clips, click the “Transitions” tab in the left pane, type “mix” in the search field, and drag the mix transition icon down to the timeline between two clips. If necessary, zoom the timeline out. You’ll see a semi-white section between the two clips representing the transition. If you move the playhead to that transition area in the timeline, you can see a preview of the transition in the preview pane, showing a blending of the two clips.

If you want to apply the same transition to all clips, click one transition in the timeline and then, in the right pane, click the “Apply to all” button.

7. Add background music

  1. Click the “Audio” tab in the left pane.
  2. Click the “Import” button to import songs (MP3) from your computer.
  3. Drag a song from the list of songs down to the timeline below the main video track.

As with still images and video clips, you can trim audio clips in the same manner.

If you want to crossfade two adjacent songs,

  1. put the 2nd song on a separate track below the first song’s track
  2. zoom in on the timeline and scroll to where the two songs meet
  3. click the 1st song and, in the right pane, set a fade out duration of, say, 1 second
  4. click the 2nd song and, in the right pane, set a fade in duration of, say, 1 second
  5. position the playhead just before the end of the first song
  6. preview the audio crossfade in the preview pane

Notice the black curve in the audio tracks showing the fade effect.

When you add audio or music track, if your video clips contain audio, you’ll hear audio from all tracks containing audio. If you want to mute all audio from all clips in the main track, click the audio icon as shown below.

If you want to adjust the volume or mute just a single video clip, click on the clip in the timeline, then in the right pane, click the “Audio” tab and drag the volume slider.

8. Adjust color and lighting

You can adjust the color and lighting for both image and video clips individually.

  1. Click on a clip in the timeline
  2. Move the playhead to that clip
  3. In the right pane, click “Adjust” > “Basic” > “Auto adjust”
  4. Slide the “Auto adjust” intensity slider until you like how the clip looks in the preview pane.

You can also manually adjust the color by tweaking various color settings in the right pane.

I find adjusting color is especially helpful for brightening a dark image or video. Here’s how one clip looks before and after applying “auto adjust”.

Before
After

9. Stabilizing a shaky video

If your video footage was taken with a camera that doesn’t include mechanical stabilization (like a gimbal) or software stabilization, then the resulting footage could be annoyingly shaky. Capcut can try to stabilize your footage at the expense of cropping a portion of the video. To stabilize a shaky video,

  1. click on a video clip in the timeline
  2. in the right pane, click the “Video” tab and check the “Stabilize” option.

10. Animate images

For certain photos, I like to apply a subtle zoom-out animation effect to them. To do this,

  1. click on the image clip in the timeline
  2. move the playhead in the timeline to where the image clip is
  3. click the up arrow key to move the playhead to the beginning of the clip
  4. in the right pane, click “Video” > “Basics”, change the “scale” value to 200% and click the diamond icon to set a keyframe
  5. click the down arrow key to move the playhead to the end of the clip
  6. in the right pane, click “Video” > “Basics” and change the “scale” value to 100%
  7. drag the playhead from the beginning to the end of the clip. You should see the image zoom out. You can also click the play button in the preview pane to preview the animation.
Playhead is at the beginning of the clip
Scale at 100%
Scale at 100%
Scale at 200%
Scale at 200% with keyframe set
Playhead at end of clip

11. Create rolling credits

There are different ways to create rolling credits. The way I’m about to show you involves slowly animating an image’s position upwards. Therefore, you’ll need to create a tall image with the content you want in it, like this

  1. Add this image to the timeline
  2. Select the image in the timeline
  3. Position the playhead in the timeline where the image is
  4. Click the up arrow key to move the playhead to the beginning of the clip
  5. In the right pane, click “Video” > “Basics”
  6. Change the “scale” value until you like how the credits image looks in the preview. In the screenshot below, I set it to 500%.
  7. Change “position” Y value to a value that moves the top of the image near the bottom of the preview pane. In the screenshot below, I set it to -3300.
  8. Click the diamond icon to set a keyframe
  9. Click the down arrow key to move the playhead to the end of the clip
  10. in the right pane, click “Video” > “Basics” and change the “position” Y so that the bottom of the image is near the top of the preview pane. In the screenshot below, I set the value to 3300.
  11. Drag the playhead from the beginning to the end of the clip. You should see the image roll up. You can also click the play button in the preview pane to preview the animation.

12. Export your video

Click the light blue “Export” button at the top right to export the video.

Easily Remove Grout Haze From Tile

When applying grout to tile, you’re supposed to wait 15-30 minutes before wiping the grout of the tile while leaving the grout between each tile. This is easier said that done. No matter how good you are, you’ll probably be left with a thin layer of grout on your tile. If you use a light-colored grout on light-colored tile, you won’t notice the haze except at certain angles in certain lighting. The haze can resemble hard water stains. If you use a dark-colored grout on light tile or a light-colored grout on dark tile, the haze is more prominent, and your tile can look dirty. See example below.

Before

You can try to scrub with soap or even a steam cleaner, but that will have zero effect. Luckily, there is an amazing chemical product that can relatively easily remove the grout haze from tile. With Aqua Mix 1 Qt. Cement Grout Haze Remover by Custom Building Products, you just mix the liquid with water, pour some one the tile, wait a while, then rub the haze off. For tougher haze, don’t mix with water. In my case, I poured the liquid at full strength without adding water into a spray bottle, sprayed the liquid on the tile, then wiped the haze off with a rag. As most comments say on the Home Depot product page, this stuff is “amazing”!

After
Aqua Mix 1 Qt. Cement Grout Haze Remover by Custom Building Products

Quickly and Easily Remove Weeds From Your Driveway

To easily remove weeds from control joints in your concrete driveway, use an angle grinder with a wire wheel. I use a 4.5″ Ryobi brushed, cordless angle grinder. Don’t use a brushless angle grinder because it can’t be used for this purpose. I prefer using a knotted wire wheel. A non-knotted wire wheel works as well, but I find it better for cleaning control joints after removing large weeds using the knotted one.

Knotted Wire Wheel

Non-knotted Wire Wheel

To prevent kickback, stand and drag the tool to the right as shown in the photo below.

If the angle grinder flange lock nut is stuck, use either a pipe wrench or a thin wrench to unlock it. For convenience, clamp the angle grinder to a stationary vise.

English Muffin Breakfast Recipe

A few years ago, I flew to Korea on Hawaiian Airlines and was served a warm English muffin containing a turkey patty and egg. As cheap and simple as it looked, I wasn’t expecting much, but it turned out to be so good, I had to try to recreate it. I think I figured it out. Here’s my recipe.

Ingredients

Instructions

  1. Spray some cooking oil on a frying pan.
  2. Fry the egg in the shape of the muffin. I like to use this egg pancake frying pan that I got on Amazon. Optionally, pierce the yolk.
  3. Sprinkle some salt and pepper on the egg and cover the pan so the top of the egg gets cooked.
  4. Slice the muffin into 2 halves.
  5. Heat the muffin in a microwave for 1 minute to warm and soften it up. This is especially necessary if the muffins were refrigerated, cold, and hard.
  6. Since the turkey patties are small, defrost 2 of them in a microwave for 2 minutes and 30 seconds.
  7. Spread some hummus on all inner sides of each muffin half.
  8. Cut one of the turkey patties in half so that one and a half muffins can cover most of the muffin.
  9. Put 1.5 turkey patties next to each other, cover it with half a cheese slice, and microwave for 25 seconds.
  10. Assemble the muffin as shown in the photos below.

A Collection of AI Tools for Various Purposes

This is just a collection of AI tools I’ve used that I’ve found useful. With so many AI tools sprouting up, this list will likely be updated regularly as time permits.

AI Portal

Image Editing

Audio Editing

Speech

Video Editing

Text Editing

  • ChatGPT
  • Grammarly

Code Editing

  • Cursor
  • Bolt.new
  • Bolt.diy
  • Claude 3.7 Code
  • GitHub Copilot
  • Coedium’s Windsurf
  • Locofy.ai
  • Vercel v0
  • Dora.ai
  • Relume
  • replit
  • lovable

Install AI Apps

Color Palette

Virtual Staging

Different Ways to Build a Component-Based Static Website

Static websites are fast and ideal for many types of websites like blogs, marketing websites, and more. When building websites, you should always use a component-based approach for code simplicity, maintenance, and reusability. Here are a few ways to build a static website using components.

  1. Web Components – No framework
  2. Eleventy (11ty) – Very simple and flexible static site generator that supports various template languages (Handlebars, Nunjucks, etc)
  3. Astro – Very similar to Eleventy, with the advantage of being able to load premade components from React, Vue, Svelte, etc.
  4. Svelte – More like React, but better. Outputs static files. Has native benefits like CSS optimizations and the ability to create interactive apps, if necessary.

The following video does a great job in comparing and demonstrating building a static site using web components and Svelte.

FrontLobby: Motivate Tenants to Pay Rent on Time and to Pay Their Rent Debt After Moving Out

I recently rented out an apartment to a new tenant. I created the lease online using Zillow. While Zillow does include mandatory notices and includes a credit score and background check service, it doesn’t mention anything about recommending landlords get certain personally identifying information (PII) that can be critical in helping landlords if a tenant decides to stop paying rent.

In one of my rentals, I inherited a tenant who, after a few years, couldn’t pay the rent. Despite trying to work with her, she clearly knew that she could stay just stop paying and stay in the apartment until she is served a formal eviction notice by the sheriff. Due to court backlogs, it would be about 2 months before the sheriff could serve her, and there was nothing I could do about it. I eventually lost around $5000, but at least I was able to renovate the apartment and rent it out for $500 a month more. Nevertheless, the carelessness of the tenant, who thought she could rip me off, was disturbing, as California law is too lenient on renters. Fortunately, I found a way that could either get the former tenant to pay her debt or suffer the consequences of her non-payment being recorded in her credit history. However, you can’t report your tenant to the credit bureaus unless you have enough information, including their date of birth. Since I didn’t have all that information, I had to do some investigative work using Instant Checkmate and Ancestry.com. With Instant Checkmate, I was able to find all sorts of information about my former tenant, including the fact that she had filed for bankruptcy and had been evicted in the past. Instant Checkmate looks like a super shady website, but luckily, it’s not. With Ancestry.com, I was able to get her date of birth, which is required in order to report people to the credit bureaus. With this information, along with signed copies of the lease and other details, I used a service called FrontLobby to go after my former tenant.

FrontLobby can be used to report current tenants to the credit bureaus when they pay late or don’t pay at all. It can also be used to report former tenants.

New / Existing Tenants

If you will have a new tenant, make sure you get the following information from them before they move in:

  • Legal first, middle, and last names
  • date of birth (required in order to report people to the credit bureaus
  • social security number (for more accurate reporting)
  • email
  • phone number
  • aliases, if any.

I would make a copy of their social security card and driver’s license.

You can then inform your new and existing tenants that you will use FrontLobby to track their rent payments and to automatically notify the credit bureaus if they are late. This should make tenants become more motivated to pay on time. You can also inform tenants that if they pay on time, that could help improve their credit score.

Former Tenants Who Are Delinquent

If your tenant has already moved out and they owe your rent, you can use FrontLobby to report their debt to the credit bureaus.

FrontLobby will first email them an introduction to encourage them to pay their debt.

FrontLobby will send the former tenant up to 3 introduction emails. If the former tenant doesn’t register with FrontLobby, FrontLobby will send monthly emails informing the former tenant that their credit score will continue to be affected until they pay.

Lastly, if the former tenant still doesn’t pay, then they will get an email alerting them that their debt has been registered with the credit bureaus.

Once you activate debt reporting, you’ll see a notice like this:

Using a Grid System for Web Design and Development

I’ve worked with many graphic designers who were tasked with created web designs. Unfortunately, most designers claimed to know web design when in reality they didn’t. This was obvious when they kept asking me for specific width and height dimensions when I’d ask them for a new hero design. Designing for print is much simpler than designing for web. With print, what you see is what you get because there’s only one size and nothing is interactive. In fact, there are numerous other factors that must be considered when designing for web, including SEO impact. In this post, I’ll talk about just one aspect of design that all web designers should understand: grid systems.

A grid system is just a bunch of columns and, optionally, rows, that help you arrange your design elements. It’s useful for print design, but I’d argue it is essential for web design. In the New York Times screenshot above, you see a bunch of pink columns separate by white gaps (gutters). Notice how the content blocks fit within the columns. Without those columns, your design could end up with a lot of alignment issues, especially when the design contains a lot more than just a bunch of text blocks. Grid systems don’t just make it easier for designers to align content – they also make it easier for developers to develop responsive pages that match the designs they are provided. That’s why CSS has a display option called “grid” and why Bootstrap, one of the most popular CSS frameworks, offers a grid system with sensible defaults.

The most common grid system is the 12-column grid. If you’re a developer, you don’t have to use CSS grid or a premade system like Bootstrap’s grid system – you can use flexbox to lay out your content. But I personally think it’s better to use a grid for your main layout and just use flex for sub-layouts, e.g., when you’re laying out content within a grid’s cell. This is particularly helpful because your main layout will need to be responsive, and Bootstrap’s grid system already includes code to make your grid responsive automatically. Additionally, if you are part of a dev team, it’ll be easier to update someone else’s code if everyone follows the same coding convention, like using Bootstrap’s grid classes.

If you use Tailwind CSS, you can easily create a grid system using Tailwind CSS classes. However, you’ll have to define your own breakpoints, e.g., on desktop, show 12 columns, but on mobile, show only one.

The easiest way to demonstrate both Bootstrap’s grid system and creating a grid in Tailwind CSS is by example. If you are a developer, the CodePen below should be self-explanatory. Open each CodePen in a separate tab to see the columns on desktop.

Tailwind CSS

See the Pen Tailwind CSS Layouts by Abdullah Yahya (@javanigus) on CodePen.

Bootstrap

See the Pen Untitled by Abdullah Yahya (@javanigus) on CodePen.

Set Up a Component-based Static Site Generator That Uses JSON Files to Store Page Content Instead of a CMS

There are many options for a content management system (CMS). WordPress is the most popular one. Its native WYSIWYG block editor CMS is called Gutenberg. I’m using it right now as I’m typing these words 🙂 WordPress is more than just a CMS, it’s a platform that you can heavily customize and that you can add additional WYSIWYG CMSs on top of it, e.g. WP Bakery, Elementor, and many others. You can even create a non-WYSIWYG CMS in WordPress using Advanced Custom Fields (ACF), which would give you something similar to dedicated non-WYSIWYG CMSs like Contentful. Aside from WordPress, there’s a plethora of WYSIWYG CMSs like Webflow, Wix, etc. Then there are headless CMSs like Contentful, Strapi, Directus, and many more. WordPress can even be used as a headless CMS because it has an API from which you can get all content. This post will share a simple alternative to these complex CMS solutions by using a static site generator (SSG) called 11ty along with JSON files to store content in what you can consider a poor man’s CMS. Why? Because

  1. Faster updates
    CMSs, whether WYSIWYG or not, are for non-technical users (like most marketing people). I’ve worked in marketing for 14 years with many, many marketers. The vast majority of them do not want to update a website themselves. So, if developers have to update the website, there’s no need for a CMS. As a developer myself, the abstraction layer that a CMS provides just slows me down. Elementor, for example, is one of the most popular WYSIWYG CMSs. It doesn’t give you the ability to edit the raw code. Everything must be done visually. That can slow me down if I can’t easily find how to do what I need using the Elementor UI.
  2. Security
    WordPress is known for being vulnerable to hacks, not necessarily because of WordPress itself but because of the many plugins that people install in them. Static websites are more secure than any dynamic website.
  3. Performance
    WordPress websites are dynamic (content is queried from a database and PHP files must be processed on the server before they are delivered to clients). Static websites, on the other hand, require no processing. Of course, caching can improve the performance of dynamic sites, but static sites are still faster.
  4. Simplicity
    WordPress is way more complex than a simple static site, including a static site built by a static site generator like Eleventy. If you add a CMS on top of WordPress, then it becomes even more complex. If you use a static site generator along with a headless CMS like Contentful, then you have another dependency and layer of complexity due to needing to set up content models and then fetch them using APIs. The setup I will show you in this post will allow developers to see their entire website structure in a familiar file system without any CMS UI to get in their way. No CMS will alter code in any unintended way.
  5. Lower learning curve
    If developers have to update a website instead of non-technical marketers, then a static website offers a lower learning curve because all developers already understand website code. On the other hand, not every developer is fluent in WordPress or one of the many CMS plugins it offers, along with whatever customizations may have been made, so it would take them longer to learn these CMSs.
  6. Cost
    The setup I’m using is free because 11ty is free (open source), GitHub is free, and Netlify (for web hosting) offers a free plan. While you can host WordPress for free and use a free open-source CMS, you’ll have to maintain them, which you probably don’t want to do. Many companies go with a managed WordPress host like WP Engine, but they can be somewhat expensive. The Contentful CMS can also be expensive depending on the number of users and records.

Rather than explain how I created this starter website that uses Eleventy + JSON, I will just explain how the code, which is in this GitHub repo, works. If you want to learn how to set up an Eleventy website, just read the 11ty docs. It’s very simple.

Let’s go!

1. Install NodeJS

https://nodejs.org/en/download

2. Install Git

https://git-scm.com/downloads

3. Set Up a New Website Project Folder

mkdir test-website
cd test-website

4. Clone This Git Repo

git clone https://github.com/javanigus/eleventy-json-starter.git

5. Install Dependencies

npm install 

6. Run Eleventy

npm start

11ty will start a local web server. Open the localhost URL and view the starter site on your machine.

The top portion shows the starter site with a few menu links and some body content. At the bottom is a dump of all variables available to the page template, e.g. home page, about page, etc.

File Structure

  • “dist”, short for “distribution”, is the build output folder.
  • “src”, short for “source”, is where your source code goes
  • .eleventy.js is the Eleventy configuration file
  • the other files are self-explanatory

Eleventy Configuration

The Eleventy config file is .eleventy.js. I’ve tried to keep it as simple as possible. The fewer dependencies, the less the maintenance and the fewer the things that could break. Now, I personally would add PostCSS and Tailwind CSS to this setup if I were using it for work, but that’s beyond the scope of this starter site.

This code basically

  • tells Eleventy which file types to copy from the src folder to the dist folder
  • adds some debugging capabilities
  • tells Eleventy which folder is the source (“src”) and build output (“dist”)
  • tells Eleventy which folder it uses for includes / partials / components (“_includes”)
  • tells Eleventy which folder it uses for layouts (“_layouts”). To keep this starter simple, I’m not using any layouts.
module.exports = function(eleventyConfig) {
    const inspect = require("util").inspect;
	
    eleventyConfig.addPassthroughCopy("src", {
		//debug: true,
		filter: [
			"404.html",
			"**/*.css",
			"**/*.js",
			"**/*.json",
			"!**/*.11ty.js",
			"!**/*.11tydata.js",
            "!**/*.11tydata.json",
		]
	});
  
	// Copy img folder
	eleventyConfig.addPassthroughCopy("src/img");

	eleventyConfig.setServerPassthroughCopyBehavior("copy");

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

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

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

Website Source Files (src folder)

The “src” folder is where you put your website files (HTML, CSS, JS, etc). Instead of HTML files, I’m using Nunjucks files (“njk” extension). You can use other templating languages like Handlebars, but I prefer Nunjucks. You can think of Nunjucks as a simple version of PHP. It allows you to add logic and loops and output variables.

_data folder

The _data folder contains global data, whether it’s data returned from a JavaScript file (data.js) or from JSON files (data.json, pressReleases.json). This data is available to all page templates.

_includes folder

The _includes folder is where I put shared code (header.njk, footer.njk) and components (section1.njk, section2.njk). Section 1 can be a hero component and section 2 can be a features component, for example.

css folder

The css folder contains sitewide CSS (global.css) and CSS for each component (header.css, section1.css, etc).

js folder

The js folder contains sitewide JavaScript (global.js) and JavaScript for each component (header.js, section1.js, etc).

Other files and folders

The other files and folders in the “src” folder correspond to each page on the site,

  • src/index.njk (home page at /)
  • src/product1/index.njk (product page at /product1)
  • src/product1/support/index.njk (product support page at /product1/support/)
  • etc

Home Page

Home Page-specific files

  • src/index.njk (you can think of this as index.php)
  • src/index.css (this CSS file is only needed if you have CSS that is exclusive to the home page)
  • src/index.js (this JS file is only needed if you have JS that is exclusive to the home page)
  • src/index.data.json (this is the JSON file that contains all component data that is used in the page)
---js
{
  variable1: "value1",
  eleventyComputed: {
    datum(data) {
      return data;
    }
  }
}
---

<html>
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/modern-normalize/3.0.1/modern-normalize.min.css" integrity="sha512-q6WgHqiHlKyOqslT/lgBgodhd03Wp4BEqKeW6nNtlOY4quzyG3VoQKFrieaCeSnuVseNKRGpGeDU3qPmabCANg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="stylesheet" href="/css/global.css" />
    <link rel="stylesheet" href="/css/header.css" />
    <link rel="stylesheet" href="/css/footer.css" />
    <link rel="stylesheet" href="/css/section1.css" />
    <link rel="stylesheet" href="/css/section2.css" />
    <link rel="stylesheet" href="/index.css" />
</head>
<body>
    {% include "header.njk" %}

    {% include "section1.njk" %}

    {% include "section2.njk" %}

    <section>
        <p class="localCss">Test local CSS file</p>
    </section>
    
    {% include "footer.njk" %}

    <script src="/js/global.js"></script>
    <script src="/js/header.js"></script>
    <script src="/js/footer.js"></script>
    <script src="/js/section1.js"></script>
    <script src="/js/section2.js"></script>
    <script src="/index.js"></script>
</body>
</html>

<!-- for debugging data -->
<div style="padding: 1em;">
<h2>Dump of all data</h2>
<pre style="white-space: pre-wrap; word-wrap: break-word;"><code>{{ datum | debug }}</code></pre>
</div>

The front matter at the top above the <html> tag is just some code to help with debugging. It goes with the debugging code block at the bottom. Together, this dumps all variables, including global data, to the bottom of the page in the browser.

In the <body> section, I’m including 4 components

  • header.njk
  • section1.njk
  • section2.njk
  • footer.njk

You can think of these as including PHP files in another PHP file. Instead of PHP, it’s Nunjucks.

In the <head> section, I’m including all CSS that I need, including the CSS for the components that I’m using (header.css, section1.css, etc).

At the bottom of the <body> section, I’m doing the same thing for JavaScript.

Now, you might be thinking that this approach could load a bunch of individual CSS and JS files. As is, it would, but you can easily just add a step to your build system to optimize (bundle and minify) all CSS and JS files in just 2 files, one for all CSS and one for all JS. That’s beyond the scope of this post.

So far, this should be straightforward. Before we look at the index.data.json file, let’s look at the components that the home page is using (section1.njk and section2.njk). For demo purposes, I kept them simple.

section1.njk

<section id="section1">
    <h1>Welcome. {{section1.title}}</h1>
</section>

This should be self-explanatory. You’re just outputting a variable just like you would in PHP.

section2.njk

<section id="section2">
    <h2 {% if section2.textColor %} style="color: {{ section2.textColor }};" {% endif %}>Features</h2>
    <ul>
        {% for feature in section2.features %}
            <li><a href="{{feature.link}}">{{ feature.text }}</a></li>
        {% endfor %}
    </ul>
</section>

This section demonstrates the use of conditional logic and looping.

Looking at the 2 components, we know what variables exist, so we can write our JSON data file with corresponding values.

index.data.json

{
    "section1": {
        "title": "This is the home page."
    },
    "section2": {
        "textColor": "red",
        "features": [
               {
                    "text": "Feature 1",
                    "link": "/forms/vmdr/"
               },
               {
                    "text": "Feature 2",
                    "link": "/forms/pm/"
               },
               {
                    "text": "Feature 3",
                    "link": "/forms/cmdb/"
               }
           ]
	}
}

The JSON file contains an object for each component (section1 and section2) along with variables for each.

Building Pages

If Eleventy is running, it will detect any file saves and rebuild, e.g.

Notice how all static HTML files were built and put in the output folder (“dist”). When Eleventy builds each page, it takes all available data (global data from the “_data” folder and local page-specific data (e.g. from index.data.json) and evaluates all includes (components).

The /src/product1/ and /src/product1/support/ pages are very similar to the home page. The /src/press-releases/ folder is different in that it uses pagination to generate multiple pages from a single template. Let’s look at that in more detail.

Press Releases

The relevant files are

  • _data/pressReleases.json (data file for all press releases)
  • src/press-releases/index-detail.njk (file that generates all individual press releases)
  • src/press-releases/index.njk (file that generates a listing page with links to all press releases)

_data/pressReleases.json

This file is an array of JSON objects. Each object contains data for one press release. Note that one key is “slug”. It will be used to generate the URL for the press release.

[
	{
		"title": "Press release 1",
		"slug": "press-release-1",
        "body": "This is the body of the press release 1."
	},
	{
		"title": "Press release 2",
		"slug": "press-release-2",
        "body": "This is the body of the press release 2."
	},
	{
		"title": "Press release 3",
		"slug": "press-release-3",
        "body": "This is the body of the press release 3."
	},
	{
		"title": "Press release 4",
		"slug": "press-release-4",
        "body": "This is the body of the press release 4."
	}
]

src/press-releases/index-detail.njk

This file will generate a bunch of individual press release pages that will look like this

In this file, we have some front matter that tells Eleventy to paginate (create multiple page) from the data in the “pressReleases” variable (which is from _data/pressReleases.json). The “permalink” contains the “slug” variable. It tells Eleventy the path to use as it iterates to generate each press release page.

In the <body> section, we’re just outputting the press release title and body using variables.

---
pagination:
  data: pressReleases
  size: 1
  alias: pressRelease
permalink: "press-releases/{{ pressRelease.slug | slugify }}/"
---

<html>
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/modern-normalize/3.0.1/modern-normalize.min.css" integrity="sha512-q6WgHqiHlKyOqslT/lgBgodhd03Wp4BEqKeW6nNtlOY4quzyG3VoQKFrieaCeSnuVseNKRGpGeDU3qPmabCANg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="stylesheet" href="/css/global.css" />
    <link rel="stylesheet" href="/css/header.css" />
    <link rel="stylesheet" href="/css/footer.css" />
</head>
<body>
    {% include "header.njk" %}

    <section>
        <h1>{{ pressRelease.title }}</h1>
        <p>{{ pressRelease.body }} </p>
    </section>
        
    {% include "footer.njk" %}

    <script src="/js/global.js"></script>
    <script src="/js/header.js"></script>
    <script src="/js/footer.js"></script>
</body>
</html>

src/press-releases/index.njk

In this file, we want to list all press releases with a link to each one so the page looks like this

So, we loop over the pressReleases array of JSON objects to do so.

---js
{
  variable1: "value1",
  eleventyComputed: {
    datum(data) {
      return data;
    }
  }
}
---

<html>
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/modern-normalize/3.0.1/modern-normalize.min.css" integrity="sha512-q6WgHqiHlKyOqslT/lgBgodhd03Wp4BEqKeW6nNtlOY4quzyG3VoQKFrieaCeSnuVseNKRGpGeDU3qPmabCANg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="stylesheet" href="/css/global.css" />
    <link rel="stylesheet" href="/css/header.css" />
    <link rel="stylesheet" href="/css/footer.css" />
</head>
<body>
    {% include "header.njk" %}

    <section>
        <h1>Listing of all press releases</h1>
        <ul>
            {% for pressRelease in pressReleases %}
                <li><a href="/press-releases/{{pressRelease.slug | slugify}}">{{ pressRelease.title }}</a></li>
            {% endfor %}
        </ul>
    </section>
        
    {% include "footer.njk" %}

    <script src="/js/global.js"></script>
    <script src="/js/header.js"></script>
    <script src="/js/footer.js"></script>
</body>
</html>

<!-- for debugging data -->
<div style="padding: 1em;">
<h2>Dump of all data</h2>
<pre style="white-space: pre-wrap; word-wrap: break-word;"><code>{{ datum | debug }}</code></pre>
</div>

When you save a file, Eleventy will build all pages. As you can see in the screenshot below, Eleventy created 4 press release pages, one for each JSON object in the JSON data file.

  • [11ty] Writing ./dist/press-releases/press-release-1/index.html from ./src/press-releases/index-detail.njk
  • [11ty] Writing ./dist/press-releases/press-release-2/index.html from ./src/press-releases/index-detail.njk
  • [11ty] Writing ./dist/press-releases/press-release-3/index.html from ./src/press-releases/index-detail.njk
  • [11ty] Writing ./dist/press-releases/press-release-4/index.html from ./src/press-releases/index-detail.njk

Eleventy also built the listing page.

  • [11ty] Writing ./dist/press-releases/index.html from ./src/press-releases/index.njk

Hosting

If you use Netlify or Vercel for hosting, you can connect them to GitHub so that whenever you push to GitHub, each will trigger a build and deploy your changes to production on a global CDN.

Updating JSON Files Reliably

Since content will be in JSON files, you may wonder how easy it would be to edit them without breaking the JSON format. If this is your concern, you can always just copy and paste the JSON code into an online JSON editor like this one. On the left is the JSON content in “code” format and on the right is the same content in “tree” format. In the “tree” format, you can conveniently expand and collapse nodes (in case some are too long) and safely edit the name/value pairs without worrying about breaking the JSON format. If the “Live” toggle is enabled, you can see your changes in both panes updated automatically. When you’re done editing in “tree” view, you can just copy/paste the code in “code” view back to your code editor.

If you need to put HTML in a JSON value, you’ll need to escape the HTML first. you can use an online tool like this one to do that. Just paste the HTML in the top field, click “Escape JSON”, and get the escaped HTML in the bottom field.

If you need an easy way to get HTML from a visual text editor like MS Word or a Google Doc, you can use EditorHTMLOnline. Just type your content on the left and then copy the HTML on the right.

Here’s an example.

If you want to give users a simple web form with validation and select fields, you can use JSON editor.

Conclusion

Now, you can create components using simple HTML, CSS, JS, and Nunjucks (which is similar to JavaScript) and you can store all your data in simple JSON data files rather than some database or external headless CMS. The entire system is super simple but effective without a very low learning curve and zero abstraction.

Bolt: Save Time Coding Using This Agentic AI Tool

Coding web pages by hand is time-consuming. I’ve tried a few AI-based coding tools like Claude.ai, Ninja AI, and Bolt. Bolt seemed to produce the best results. It’s not perfect, but it definitely can serve as a good starting point. To demonstrate, let’s see how each of these AI tools generate code for this simple section.

For each tool, I’ll upload the same screenshot of the section and provide the same prompt, namely:

Write plain HTML and Tailwind CSS code to create the uploaded screenshot exactly.

Claude.ai (using Claude 3.5 Sonnet)

Here’s the output.

Claude can’t show a preview, so I copied and pasted the code into Codepen. Here’s how it looked.

That’s actually not bad. The image is missing because it’s a placeholder image to a relative path that doesn’t exist.

Ninja AI

For the models, I chose Claude 3.5 Sonnet for the external model. Ninja AI will combine it with its own internal models. Here’s the input.

And here’s the output.

Like Claude, Ninja AI can’t show me a preview, so I copied and pasted the code into CodePen. Here’s what it showed.

Not bad, but it’s not as good as Claude even though I chose Claude as the external model. The main issue is the vertical spacing between the elements on the right.

Bolt.new

Here’s the input.

Bolt can show a visual of what the code would produce. Here’s the code output.

Note that Bold will install a Vite and a bunch of dependencies like Tailwind CSS, Autoprefixer, PostCSS, etc. Here’s the visual preview output.

Conclusion

I’ve run a bunch of other tests comparing all 3 AI tools. Bolt is better than the other tool for code generation.

Bolt.diy

The problem with all of the above AI coding tools is they can become expensive. Luckily, there’s an open-source version of Bolt called Bolt.diy. It can be used with any LLM, including the free, experimental version of Google Gemini Pro 2.0 and DeepSeek. You can install bolt.diy by following the simple instructions at https://github.com/stackblitz-labs/bolt.diy. When you run bolt.diy, it will open in a local browser.

Let’s try a couple of LLMs with bolt.diy to code the same section above.

Google Gemini Pro 2.0 Experimental

To use Google Gemini Pro 2.0 Experimental, you’ll need to get an API key. Go to OpenRouter.ai, search for the LLM, and get a free API key.

Here’s the input.

While writing the code, bolt.diy returned an error.

I clicked “Ask Bolt”, it Bolt self-corrected. Here’s the code output.

And here’s the visual preview.

This does not look good at all. Let’s try DeepSeek Coder.

DeepSeek Coder

We’ll need an API key. Go to the DeepSeek platform, sign up, and get a key.

Here’s the input in bolt.diy with DeepSeek selected.

And here’s the output.