I’m currently migrating a large website from Handlebars to Nunjucks. Since the website is being updated daily, and because there are too many pages, I can’t convert the Handlebars syntax to Nunjucks syntax manually. To solve this, I started writing a script to convert the syntax programmatically using JavaScript (nodeJS). So far, it’s working very well. Here’s how I’m doing it, and how you can do something similar when confronted with a migration project.
Basically, the way it works is
it recursively finds all files in a folder called “temp”
if the file path ends with “hbs” – indicating it is a Handlebars file – then for each file, it executes a series of regex search and replace commands, e.g.
replace {{#if class}} with {% if class %}
replace {{/if}} with {% endif %}
and so on.
Those are simple search-and-replace situations. There may be a situation where you’ll need an advanced search and replace, e.g. when replacing
{{> social-list
dark="true"
centered="true"}}
with
{% set dark="true" %}
{% set centered="true" %}
{% include social-list.njk %}
In this case, you can use a “replacer” function, which allows you to do much more to manipulate the output.
When you’re all done and you’ve built the HTML files from both the handlebars templates and the nunjucks templates, you can write a script that recursively reads all HTML files in the build output folder and lists each HTML file path generated from each handlebars and nunjucks template along with their respective file size. The file sizes should be the same or almost the same. If some are not, then the migration script didn’t convert those templates correctly. Maybe something like:
Capital gains is the profit you make from selling a capital asset.
Capital assets include stocks, bonds, precious metals, jewelry, art, and real estate.
Selling a capital asset after owning it for one year or less results in a short-term capital gain.
Selling a capital asset after owning it for more than one year results in a long-term capital gain.
Net capital gains are calculated based on your adjusted basis in an asset. This is the amount that you paid to acquire the asset, less depreciation, plus any costs that you incurred during the sale of the asset and the costs of any improvements that you made.
Short-term capital gains are taxed as ordinary income from your salary or wages.
Long-term capital gains are subject to a tax of 0%, 15%, or 20% (depending on your income).
Short-Term Capital Gains Tax Rates By Income (2023)
Filing status = single
Income
Short-Term Tax Rate (same as ordinary income tax rate)
Up to $11,000
10%
$11,000+ to $44,725
12%
$44,725+ to $95,375
22%
$95,375+ to $182,100
24%
$182,100+ to $231,250
32%
$231,250+ to $578,125
35%
Over $578,125
37%
Filing status = married filing jointly
Income
Short-Term Tax Rate (same as ordinary income tax rate)
Long-Term Capital Gains Tax Rates By Income (2023)
Filing status = single
Income
Long-Term Tax Rate
Up to $44,625
0%
$44,626 to $492,300
15%
Over $492,300
20%
Filing status = married filing jointly
Income
Long-Term Tax Rate
Up to $89,250
0%
$89,251 to $553,850
15%
Over $553,850
20%
Taxable Income
Taxable income is the portion of your gross income that the IRS deems subject to taxes.
It consists of both earned income and unearned income.
Taxable income is generally less than adjusted gross income because of deductions that reduce it.
Taxable income = Gross income – deductions
For a business, revenue – business expenses = profit. Profit – deductions = taxable income.
Deductions
The IRS offers individual tax filers the option to claim the standard deduction or a list of itemized deductions.
Standard Deduction
The standard deduction is a set amount that tax filers can claim if they don’t have enough itemized deductions to claim. For the 2022 tax year, individual tax filers can claim a $12,950 standard deduction ($13,850 for 2023). If you are married filing jointly, the standard deduction is $25,900 ($27,700 for 2023).
Itemized Deductions
If you plan to itemize deductions rather than take the standard deduction, these are the records most commonly needed:
Property taxes and mortgage interest paid (form 1098)
State and local taxes paid (this is on form W-2 if you work for an employer)
Charitable donations
Educational expenses
Unreimbursed medical bills
Documents related to operating a rental property, such as receipts for repairs, advertising, etc. Learn more.
A tax credit will lower your tax liability (any taxes you owe). If you don’t owe any taxes, then you may or may not get a refund, depending on the tax credit details. Some tax credit examples are
30% credit off the total cost of a solar panel installation
$7,500 tax credit when you buy a qualifying electric vehicle
Refundable tax credit: If your tax credit is refundable, then even if you have no tax liability, e.g. if you are retired, then you will still get a refund for the entire tax credit amount.
Non-refundable tax credit: If your tax credit is non-refundable, then you will only get the full credit if your tax liability is at least as much as the tax credit. For example, if your tax credit is $1000 and your total tax liability before applying the credit is $1500, then your updated tax liability is reduced to $500. However, if your total tax liability before applying the credit is $800, then your updated tax liability is reduced to $0 and you will NOT get a refund for $200. For that reason, you should ensure your tax liability is at least as much as the tax credit you want to apply.
How to Calculate Taxable Income
Determine Your Filing Status
single,
married filing jointly,
etc
Gather Documents for all Sources of Income
form W-2 for earned compensation
form 1099-INT for interest income
etc
Calculate Your Adjusted Gross Income (AGI) Your AGI is the result of taking certain “above-the-line” adjustments to your gross income, such as contributions to a qualifying individual retirement account (IRA), student loan interest, and certain education expenses. These items are referred to as “above the line” because they reduce your income before taking any allowable itemized deductions or standard deductions.
Calculate Your Deductions (Standard or Itemized)
Calculate Taxable Income Taxable income = AGI – deductions
Marginal vs. Effective Tax Rates
The US has a progressive tax system, so your income is taxed at different rates. If your total annual income is $125,000 and your taxable income is $100,000 (income minus deductions and credits) and you are filing single, your tax liability would not be 24% of the entire $100,000. Instead, it would be $17,400. In this case, your “effective” or “average” tax rate is $17,400/$125,000 = 13.9%, which is much lower than 24%.
If your car seats, floor mats and furniture are made of upholstery, they will eventually get dirty. Fortunately, cleaning them isn’t too hard if you use the right tools and technique. I recently cleaned an old car seat and floor mats. The process was simpler than I thought. Here’s a before and after pic.
I also rented the Karcher Carpet Cleaner and Detailer from Home Depot to compare it to the Bissel.
Both of these tools have 2 functions;
Squirter: to squirt water or cleaning solution
Extractor: to suck water from upholstery
You could also just use a spray bottle to squirt the cleaning solution.
You can also use a wet shop vac, but it’s helpful to have a hose head that is transparent so you can see if any water is left in the upholstery. Here’s a transparent extractor nozzle designed to fit all shop vacs.
The Bissel nozzle includes a brush, but it’s better to use a drill brush. For the drill brush, I went with this one on Amazon. Make sure to choose brushes designed for upholstery.
This one includes 4 items:
2″ brush
3.5″ brush
4″ brush
Drill extension
Instructions
Add the cleaning solution to the carpet cleaner tool.
Spray the cleaning solution onto the item to be cleaned.
Use a drill brush to agitate the soiled upholstery. I find that using the 3.5″ brush sideways does a better job at agitating upholstery.
Suck any moisture using the carpet cleaner tool.
I added a cleaning solution to the carpet cleaner tool to spray the solution onto the upholstery.I also bought and applied a upholstery cleaning foam.The 4″ brush is also effective in agitating the soiled fabric.Here’s a stain that was actually easy to remove.This time, I use the 3.5″ brush sideways.After sucking the moisture, the stains were gone.
When cleaning upholstery in this way, not a lot of water is needed. After sucking the water, very little is left, so it doesn’t take long for the material to dry.
This is the nozzle on the Bissel.This is the nozzle on the Karcher.
According to RocketMortgage, the average mortgage term is 30 years and the average length is under 10 years. This is because homeowners will either refinance their home, like I did to get a much lower interest rate, or because they want to move. Refinancing your mortgage to get a lower interest rate makes sense if the new rate will be much lower such that you’ll end up saving money. You’ll just have to keep in mind that if you’ve had your mortgage for 10 years, for example, and you refinance, the clock resets and you’ll have 30 years to pay off your mortgage instead of 20. For this reason, I personally continue to pay the same monthly payments at the higher interest rate so that I pay less interest and the mortgage is paid off sooner.
But what if you sell your home to buy a new one within 10 years? What many people may not realize is that by doing this, they will lose a lot of money because their home loan is an amortized loan rather than a simple interest loan. Amortized loans favor lenders, like banks, instead of borrowers. Unlike a simple interest loan, where you’re paying the same amount towards principal and interest each month, when you get a mortgage, most of your monthly payments go towards interest in the beginning and less near the end of the 30-year term.
Note that your total interest payments over 30 years is more than the loan amount.
Let’s take a few points in time and compare how much of your monthly payment goes towards principal and interest.
Principal
Interest
Percent Towards Interest
First mortgage payment
$346
$2244
86.64%
Mortgage payment at month 100 (8.3 years)
$605
$1985
76.64%
Mortgage payment at month 200 (16.7 years)
$1058
$1531
59.13%
Mortgage payment at month 236 (19.7 years)
$1294
$1295
50%
Mortgage payment at month 300 (25 years)
$1851
$738
28.51%
Mortgage payment at month 360 (30 years)
$2578
$14
0.54%
As you can see, in the first 10 years of your mortgage, the bulk of your monthly payments goes towards paying interest. Your equity from paying down the principal is very little. Therefore, if you sell your house within the first 10 years and buy a new one, you’ll have little equity from your mortgage payments and, when you get a new mortgage for your new home, you’ll start over from month 1, when most of your new monthly payments will go towards interest again.
Of course, your house could appreciate significantly in 10 years, in which case the equity you gain from appreciation could outweigh the equity from paying off the principal. However, that is not always the case.
If you’re planning on selling your home within 10 years and buying a new one, it may not be worth it since you may lose a lot of money from having mostly just paid interest.
This itinerary assumes a starting point in Foster City, California near Qualys headquarters. It does not include every single tourist destination, but it does include many popular ones. This particular itinerary includes brunch and dinner at restaurants based on personal preference. The itinerary was designed to make a loop to avoid backtracking as much as possible.
Eggs Benedict: Grilled Canadian Bacon, Two Poached Eggs, Crispy English muffins & House-made Tomatillo Hollandaise Sauce. Served with Rosemary Garlic Potatoes or Fresh FruitEggs Florentine: Sautéed Spinach, Roasted Tomatoes, Crispy English muffins, Two Poached Eggs, Topped With House-made Tomatillo Hollandaise Sauce. Served with Rosemary Garlic Potatoes or Fresh FruitHash on Haight: Corned Beef brisket, sauteed peppers, onions, Rosemary Garlic Potatoes & 2 eggs any style topped with house-made tomatillo hollandaise sauce. Served with a side of Fresh FruitPhilly Cheese SteakFunky Monkey Crepe: Bananas, brown sugar, Nutella, ice creamBerrylicious Crepe Tiramisu Crepe Basic Dessert Crepe
With so many people working both from home and at the office, it can become annoying to have to rearrange your application windows when you move between the two locations. This is especially true for people like me who need multiple monitors, two of which are 32″ 4K ones as shown below, which I need to display multiple windows on each screen.
Though I have a similar setup at home, my application windows always get jumbled up when I move between locations, possibly because the standalone monitors are not all the same brand with the same exact resolution.
Most window management apps allow you to move and resize windows in a grid, e.g.
left 50% of screen,
bottom 50% of screen,
right 33% of screen,
top 50%, left 50% of screen,
etc
These are fine if you aren’t going to move locations often and don’t have too many windows. If you want the same layout spanning multiple monitors and the ability to instantly move and resize all windows to that layout, then I recommend Moom. Here’s how to use Moom to save layouts for multiple monitor configurations.
At location 1, e.g. work, open your applications and arrange them how you like
Open Moom and create a custom preset with the following settings
Type: Arrange Windows
Name: I put “3 Monitors – Work”
Uncheck all checkboxes
Click “Update Snapshot”
This saves the layout as a preset. To test it, resize and move all your windows around. Then, hover over the green dot in any one window and click on the preset. All windows will instantly move to how you had them.
When you’re at home, you can create another preset and call it something like “3 Monitors – Home”. Now, you no longer have to mess around with moving windows around. Just click on a preset from any open window and get back to business.
Moom has a one-time cost of $10, but it’s obviously worth it.
In this tutorial, I will explain how we can fetch remote paginated JSON data synchronously (in serial) and asynchronously (in parallel).
Data
You can get test data to fetch from RapidAPI, but I’m going to fetch video data from Vimeo using the Vimeo API.
Fetch Method
There are many ways you can fetch remote data. The RapidAPI website provides code snippets for various languages and fetch methods. For example, for Node.js, there’s HTTP, Request, Unirest, Axios, and Fetch.
Some services like Vimeo provide libraries and SDKs in a few languages like PHP, Python and Node.js. You can use those as well if you’d like.
I’m actually going to use the Got library [GitHub], which is a very popular library.
CommonJS vs ESM
Many of the latest Node packages are now native ESM instead of CommonJS. Therefore, you can’t require modules like this
const got = require('got');
Instead, you must import modules like this
import got from 'got';
According to this page, you can convert your project to ESM or use an older version of the got package that uses CommonJS.
If using ESM, you need to put "type": "module" in your package.json.
Authentication
Many services like Vimeo require authentication in order to use their API. This often involves creating an access token and passing it in the header of the API call like this
GET /tutorial HTTP/1.1
Host: api.vimeo.com
Authorization: bearer {access_token}
Setup
Let’s set up our project. Do the following:
Create a new folder, e.g. test
Open the folder in a code editor (I’m using VisualStudio Code)
Open a terminal (I’m doing it in VS Code)
Initialize a Node project by running npm init -y
This will generate a package.json file in the folder.
Since we’re using ESM and will import modules rather than require them, add the following to the package.json file.
"type": "module"
Call the Vimeo API
Let’s start by calling the Vimeo API just once. Create a new file called get-data-one.js and copy the following contents into it. Replace {user_id} with your Vimeo user ID and {access_token} with your Vimeo access token.
import got from 'got';
let page = 1;
let per_page = 3;
let fields = "privacy,link,release_time,tags,name,description,download";
const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;
const options = {
method: 'GET',
headers: {
'Authorization': 'bearer {access_token}'
}
};
let data = await got(url, options).json();
console.log(data);
We’re importing the got library. For this to work, we need to install the got package. Run the following command.
npm install got
This will download the got package and its dependencies into the node_modules folder.
In the code, the Vimeo endpoint we’re calling is /users/{user_id}/videos, which returns all videos that a user has uploaded. According to the API docs, we can
Specify the page number of the results to show using page
Specify the number of items to show on each page of results, up to a maximum of 100, using per_page
Specify which fields to return using fields
These parameters can be added to the endpoint URL in the query string, which is what we’ve done. However, for this test, we’ll just call one page and return the records (videos). We then call the API using the got library and then dump the results to the console. Let’s run the script and check the output. Run the following command.
node get-data-one.js
As expected, here’s the output.
The output starts with pagination info and the total number of available records (videos) followed by the actual data in the form of an array of video objects. In this case, we see 3 objects because we set per_page to 3.
Let’s update our code to write the output to a file. That will make it easier to read when there’s a lot of data. Add the following code snippets
import fs from "fs";
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
stream.once('open', function(fd) {
stream.write(JSON.stringify(data)+"\n");
stream.end();
});
so the code looks like this:
import fs from "fs";
import got from 'got';
let page = 1;
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download";
const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;
const options = {
method: 'GET',
headers: {
'Authorization': 'bearer {access_token}'
}
};
let data = await got(url, options).json();
console.log(data);
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
stream.once('open', function(fd) {
stream.write(JSON.stringify(data)+"\n");
stream.end();
});
We don’t need to install the fs package because that’s included in Node by default. The stream will write data to a file we’ll call video-data.json and we pass it the “w” flag to overwrite any existing contents of the file.
When we rerun the script, we see the file is created. We can format (prettify) it so it’s easy to read.
Call the Vimeo API Multiple Times in Serial with Pagination
Now, let’s say we want to fetch more data, but the API limits how many records are returned in a single call. In this case, we need to call the API in a loop passing a different page number. Let’s create a new file called get-data-serial.js with the following code.
import fs from "fs";
import got from 'got';
let data = [];
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download";
const options = {
method: 'GET',
headers: {
'Authorization': 'bearer {access_token}'
}
}
for(let page = 1; page <= 3; page++) {
const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;
let somedata = await got(url, options).json();
data.push(somedata);
console.log(page);
};
console.log(data);
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
stream.once('open', function(fd) {
stream.write(JSON.stringify(data)+"\n");
stream.end();
});
Here, I’m using a simple for loop to loop through 3 pages. I also created a data variable as an empty array. With each loop iteration, I push the page’s returned data to the data array. When all is done, I write the data array to a file, which looks like this.
I collapsed the “data” array so we can see that 3 pages of data were returned. We ran this in serial so the order of the output is page 1, page 2, and page 3.
Call the Vimeo API Multiple Times in Parallel with Pagination
Now, let’s do the same thing, but asynchronously (in parallel). Create a new file called get-data-parallel.js with the following code.
import fs from "fs";
import got from 'got';
const options = {
method: 'GET',
headers: {
'Authorization': 'bearer {access_token}'
}
};
let data = [];
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download";
let pages = [1,2,3];
await Promise.all(pages.map(async (page) => {
const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=2&fields=privacy,link,release_time,tags,name,description,download`;
let somedata = await got(url, options).json();
data.push(somedata);
console.log(page);
}));
console.log(data);
var stream = fs.createWriteStream("video-data-parallel.json",{flags:'w'});
stream.once('open', function(fd) {
stream.write(JSON.stringify(data)+"\n");
stream.end();
});
In this case, instead of a for loop, we’re using Promise.all and passing to it an array of page numbers that we loop over using the map function. When we run the script, we get output like the following:
You’ll notice 2 things:
the script runs faster because the API calls are done simultaneously in parallel (asynchronously) rather than one after the other in serial (synchronously).
the order of the output is no longer consecutive by page number. In this example, it was page 1, page 3, page 2.
Modifying the JSON Output Structure
As shown in the previous screenshot, the API call returns an object containing pagination info followed by a data array – an array of objects containing video info.
What if we just want the data objects and not the pagination info. We can do that by modifying the structure of the JSON output. We can replace
data.push(somedata);
with
data.push(somedata.data);
but then the output becomes an array of arrays.
To fix this, let’s flatten the array by adding the following code:
data = data.flat(1);
right before we console it out and write to file.
Now, the output file looks like this (each record is collapsed for visibility).
Filtering Out Certain Records
What if we want to filter out certain records, e.g. we want to filter out all videos that are not public, i.e. we only want videos where privacy.view = “anybody”. We can use the filter function to do that, like this:
Each video record can contain a lot of information, including information we don’t need. For example, the privacy object contains 5 keys.
If we want to return just one privacy key, say “view”, then we can do so using the map function as follows:
// simplify privacy object to just privacy.view
somedata = somedata.map(function (video) {
video.privacy = video.privacy.view;
return video;
});
For each video record, the “download” field is an array of objects, one for each available rendition (resolution), e.g.
If we only want to, say, return “hd” videos and only the download links, we can use two map functions like this:
// only include videos that are HD and only return HD video download links
somedata = somedata.map(function (video) {
let download = [];
video.download.map(function (size) {
if (size.quality === "hd") {
download.push({
rendition: size.rendition,
link: size.link
})
}
});
if (download.length !== 0) {
video.download = download;
return video;
}
});
Now, the downloads array is simplified, like this:
The “categories” field is an array of objects with a lot of data, including objects and arrays of objects.
What if we want to simplify that to just a comma-delimited list of category names. We can do that like this:
For reference, here’s the complete code for get-data-serial.js. The page limit and per_page values can be updated depending on how many results you want.
import fs from "fs";
import got from 'got';
let data = [];
let per_page = 2;
let fields = "privacy,link,release_time,tags,name,description,download,categories";
const options = {
method: 'GET',
headers: {
'Authorization': 'bearer {access_token}'
}
}
for(let page = 1; page <= 3; page++) {
const url = `https://api.vimeo.com/users/{user_id}/videos?page=${page}&per_page=${per_page}&fields=${fields}`;
let somedata = await got(url, options).json();
somedata = somedata.data;
// only include videos that are public
somedata = somedata.filter(video => video.privacy.view === "anybody" );
// only include videos that aren't in the "Educational" category
somedata = somedata.filter(function (video, index, arr) {
let isEducational = false;
video.categories.filter(function (category, index, arr) {
if (category.name === "Educational") {
isEducational = true;
}
});
if (isEducational === false) {
return video;
}
});
// simplify privacy object to just privacy.view
somedata = somedata.map(function (video) {
video.privacy = video.privacy.view;
return video;
});
// only include videos that are HD and only return HD video download links
somedata = somedata.map(function (video) {
let download = [];
video.download.map(function (size) {
if (size.quality === "hd") {
download.push({
rendition: size.rendition,
link: size.link
})
}
});
if (download.length !== 0) {
video.download = download;
return video;
}
});
// simplify categories array of objects to just an array of category names
somedata = somedata.map(function (video) {
let categories = [];
if (video !== undefined) {
video.categories.map(function (category) {
categories.push(category.name);
});
video.categories = categories;
return video;
}
});
data.push(somedata);
console.log(page);
};
data = data.flat(1);
console.log(data);
var stream = fs.createWriteStream("video-data.json",{flags:'w'});
stream.once('open', function(fd) {
stream.write(JSON.stringify(data)+"\n");
stream.end();
});
Most websites contain the same or very similar layouts on multiple pages, e.g. header and footer. There also might be a few different hero section designs and a few different CTA section designs. Imagine having 10 product pages each containing three 2-column sections with a text description in the left column and a screenshot in the right column. Each of these product pages may also have a CTA section design but with slightly different text and links. It’s common to put shared CSS in a shared CSS file, e.g. shared.css, common.css, or global.css. This especially makes sense for the header and footer, which are usually shown on all pages. But over time, that shared CSS file can become very long because you may have a lot of CSS for many different common sections. This can make it difficult and dangerous to edit code for just one particular section. It can also make it very difficult if you want to copy a section on one page to add to another page. If the HTML, CSS, and JS for the section aren’t isolated, you may not copy all the necessary code, not to mention you could end up with inconsistencies between two or more sections that should have the same design.
Consolidating all CSS into the fewest files possible is good for website performance (the fewer files, the fewer network requests), but nowadays, it’s common for websites, including simple static websites, to go through an automated build process to optimize the files before publishing them. The build process can do many things like minify and combine multiple CSS and JS files into single CSS and JS files, add prefixes to CSS using tools like PostCSS auto-prefixer, etc.
Following is one simple approach to grouping HTML, CSS and JS by website section. This approach can also be used for any part of a website like blocks within a section, but to keep things simple, we’ll just look at sections which I define as horizontal rows of related content, e.g.
In the src (source) folder, I’m using Nunjucks (njk) files instead of HTML files so they can include logic and pull in the components (partials). When the source files are processed, the built files show up in the “build” folder. For the home page source file (index.njk), the structure of the code could be like this
<html>
<head>
{% include "/src/components/header/header.css" %}
{% include "/src/components/footer/footer.css" %}
{% include "/src/index.css" %}
</head>
<body>
{% include "/src/components/header/header.njk" %}
... some HTML ...
{% include "/src/components/footer/footer.njk" %}
{% include "/src/components/header/header.js" %}
{% include "/src/components/footer/footer.js" %}
{% include "/src/index.js" %}
</body>
</html>
Note that the home page has its own CSS and JS files for elements that are not part of a component. When this file is built, the CSS and JS files will be combined (Netlify can do this automatically) and the included header and footer njk references will be replaced with their contents, e.g.
Here’s another example. For product page 1 (product1/index.njk), the file contents may look like this
<html>
<head>
{% include "/src/components/header/header.css" %}
{% include "/src/components/section1/section1.css" %}
{% include "/src/components/section4/section4.css" %}
{% include "/src/components/header/footer.css" %}
{% include "/src/product2/index.css" %}
</head>
<body>
{% include "/src/components/header/header.njk" %}
{% set title = "Product 1" %}
{% set heroImage = "product1.jpg" %}
{% include "/src/components/section1/section1.njk" %}
... some HTML ...
{% set text = "Try Product 1 Now" %}
{% set link = "/product1/free-trial/" %}
{% include "/src/components/section4/section4.njk" %}
{% include "/src/components/footer/footer.njk" %}
{% include "/src/components/header/header.js" %}
{% include "/src/components/section1/section1.js" %}
{% include "/src/components/section4/section4.js" %}
{% include "/src/components/footer/footer.js" %}
{% include "/src/product2/index.js" %}
</body>
</html>
In the code example above, we’re passing some variables into components section1 and section 4. That allows us to reuse a component’s layout and design while changing its content. Since product pages usually look very similar, the code for product2/index.njk might look like this
<html>
<head>
{% include "/src/components/header/header.css" %}
{% include "/src/components/section1/section1.css" %}
{% include "/src/components/section4/section4.css" %}
{% include "/src/components/header/footer.css" %}
{% include "/src/product2/index.css" %}
</head>
<body>
{% include "/src/components/header/header.njk" %}
{% set title = "Product 2" %}
{% set heroImage = "product2.jpg" %}
{% include "/src/components/section1/section1.njk" %}
... some HTML ...
{% set text = "Try Product 2 Now" %}
{% set link = "/product2/free-trial/" %}
{% include "/src/components/section4/section4.njk" %}
{% include "/src/components/footer/footer.njk" %}
{% include "/src/components/header/header.js" %}
{% include "/src/components/section1/section1.js" %}
{% include "/src/components/section4/section4.js" %}
{% include "/src/components/footer/footer.js" %}
{% include "/src/product2/index.js" %}
</body>
</html>
I reused the components but changed the value of the variables that are referenced in the components.
To prevent code conflicts, you can specify an ID in the first element of each component. For example,
section1.njk
<div id="section1">
... some HTML ...
</div>
section2.njk
<div id="section2">
... some HTML ...
</div>
Then, in the component’s CSS, to prevent CSS conflicts, you can prefix all rules like this
section1.css
#section1 .intro {
... some CSS ...
}
#section1 .features {
... some CSS ...
}
section2.css
#section2 .intro {
... some CSS ...
}
#section2 .features {
... some CSS ...
}
Similarly, with the JavaScript component file, you can do something similar, e.g.
section1.js
$("#section1 .intro")...
section2.js
$("#section2 .intro")...
Another benefit of this approach is you can create a page showing a preview of all components you have. When you want to create a new page, you can browse the list of component previews to see if you can reuse an existing component or decide if you need to create a new component.