How to Refactor Astro to Improve Code Maintainability
In “How to Create a Blog with Astro,” I explained the initial steps to create your blog with Astro, and in “How to Adapt Astro to TypeScript,” I showed you how to adjust Astro’s configurations to use TypeScript and get the most out of it (even though Astro uses TypeScript by default, I recommend configuring it this way from the start).
However…
The base template:
1. Has duplicated code:
For example, src/pages/index.astro
, src/pages/blog/index.astro
, and src/layouts/BlogPost.astro
have the following code in common:
---
import BaseHead from '@components/BaseHead.astro';
import Header from '@components/Header.astro';
import Footer from '@components/Footer.astro';
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={...} description={...} />
...
</head>
<body>
<Header />
<main>
...
</main>
<Footer />
</body>
</html>
If we want to make a change to this code, we would have to do it in all three files… for now… because if we add more “pure” pages or use layouts similar to BlogPost, the list will grow longer.
2. Is designed for a typical blog:
That is, to have some “pure” pages (like the home page, “About Me” page, …), a blog post archive, and blog posts.
We can see this in the fact that the blog page file is specifically designed for blog posts. What if I want to add another type of “serialized” publication (for example, talks I give, events, …)?
Basically, duplicate the code and go back to point 1.
What if we want something more complex? Now, we’ll see how to refactor the generated code to be easily reusable.
3. Making changes involves copying code
Watch out here! Code is code, and content is content. It’s normal that we have to add content if we want to expand what’s on the web… but duplicating or copying code?
Refactor Components
Naming Components
If you look at the src/components
folder, you’ll see components like BaseHead
or FormattedDate
… but there’s no other type of head
or date
. Additionally, in the case of the date, we assume it will always be formatted for the user.
That’s why I’m going to establish naming conventions:
- If there’s no other component that performs a similar function, we’ll avoid generating a compound name.
- If the component refers to an existing HTML tag, we’ll name it the same as the tag, respecting the casing of the language (in this case, Astro).
With this, we’re going to make some changes:
src/components/BaseHead.astro
will becomesrc/components/Head.astro
,- everything in the component corresponds to the content of the head tag,
- to maintain consistency, we’ll need to make changes in:
src/layouts/BlogPost.astro
,src/pages/blog/index.astro
,- and
src/pages/index.astro
,
src/components/FormattedDate.astro
will becomesrc/components/Date.astro
,- (there’s no other component representing dates),
- to maintain consistency, we’ll need to make changes in:
src/layouts/BlogPost.astro
,- and
src/pages/blog/index.astro
,
src/layouts/BlogPost.astro
will becomesrc/layouts/PageContent.astro
,- it’s not only used in blog posts but also in pages like src/pages/about.astro,
- we’ll rename it to
PageContent
and notContent
because Content is already used by Astro,- it’s true that when importing the component with Astro, we can name it whatever we want, but the goal is to reduce cognitive load to a minimum (i.e., to try to think as little as possible),
- to maintain consistency, we’ll need to make changes in:
src/pages/about.astro
,- and
src/pages/blog/[...slug].astro
,
Cohesive Components
If you look at the code, right now, the head tag generally only has the Head
component. If we make the tag part of the component, we gain readability because we go from
<head>
<Head />
...
</head>
to
<Head>
...
</Head>
On one hand, we’ll need to make these changes in:
src/layouts/BlogPost.astro
,src/pages/blog/index.astro
,- and
src/pages/index.astro
,
On the other hand, this means that the Head component should have at least one defined slot
, and after making the changes, the Head
component (in src/components/Head.astro
) should look like this:
---
// Import the global.css file here so that it is included on
// all pages through the use of the <Head /> component.
import '@styles/global.css';
interface Props {
title: string;
description: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
---
<head>
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<!-- Font preloads -->
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<slot />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
</head>
Centralize Configurations
Right now, the main header menu and the social menus in the header and footer are hardcoded in the files.
Therefore, when making any changes, we’ll need to modify the files by copying and adapting the code. This practice is prone to errors, so let’s refactor to avoid these errors by centralizing the menu configurations in external files.
To do this:
- Install ReactJS for Astro:
yarn astro add react
- Create the MenuItem type in a specific file we’ll call
src/types/menus.d.ts
. A menu item needs at least a display text (label
) and a path to go to (path
). In our case, since we’re also managing social menus, we’ll have an optional icon. This definition should look something like this (but feel free to modify it as needed):import { FC, SVGProps } from 'react'; export type MenuItem = Readonly<{ label: string; path: string; icon?: FC<SVGProps<SVGSVGElement>>; }>;
Note:
By defining the MenuItem type as Readonly, we are enforcing the immutability of the object. That is, we can initialize it without issues, but after that, changing its values will trigger a compile-time error. This ensures that the object does not change at any time.
- Create the Menu type and add it to the src/types/menus.d.ts file. This definition should look something like this (but feel free to modify it as needed):
export type Menu = MenuItem[];
Note:
Although we could avoid this definition (in the end, we’re just defining an array of MenuItem), any changes to this definition would require changing imports. This way, we can avoid that change in the future, which is important if every need for change is an invitation to generate errors.
- Create a folder to store the configurations. In my case, it’s the
src/config
folder. - Extract the icons to their own files in a subfolder of
src/components
(I placed them insrc/components/icons
). The GitHub icon should look like this (and will serve as an example for the others):import { type SVGProps } from 'react'; const GithubIcon = (props: SVGProps<SVGSVGElement>) => ( <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" {...props} > <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" fill="currentColor" /> </svg> ); export default GithubIcon;
- Configure shortcuts to these two new paths in the
tsconfig.json
file. After the changes, it should look like this:{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": "src/", "paths": { "@/*": ["./*"], "@components/*": ["components/*"], "@config/*": ["config/*"], "@icons/*": ["components/icons/*"], "@layouts/*": ["layouts/*"], "@styles/*": ["styles/*"] }, "strictNullChecks": true } }
- Create the menu configuration file. In my case, it’s the
src/config/menus.config.ts
file. - Extract the header menu configuration into this file. The code should look like this:
import type { Menu } from '@/types/menus.d'; export const mainMenu: Menu = [ { label: 'Home', path: '/' }, { label: 'Blog', path: '/blog' }, ];
- Now extract the social menu configuration to the
src/config/menus.config.ts
file. It should look like this:import type { Menu } from '@/types/menus.d'; import GithubIcon from '@/components/icons/GithubIcon'; export const mainMenu: Menu = [ { label: 'Home', path: '/' }, { label: 'Blog', path: '/blog' }, ]; export const socialMenu: Menu = [ { label: "Go to Template's GitHub repo", path: "https://github.com/borjalofe/astro-blog-template", icon: GithubIcon, }, ];
- Refactor
src/components/Header.astro
to use this configuration. The code insrc/components/Header.astro
should look like this:--- import HeaderLink from '@components/HeaderLink.astro'; import { mainMenu, socialMenu } from '@config/menus.config'; import { SITE_TITLE } from '@/consts'; --- <header> <nav> <h2><a href="/">{SITE_TITLE}</a></h2> <div class="internal-links"> {mainMenu.map( ({label, path}) => <HeaderLink href={path}>{label}</HeaderLink> )} </div> <div class="social-links"> {socialMenu.map( ({icon: Icon, label, path}) => ( <a href={path} target="_blank"> <span class="sr-only">{label}</span> {Icon && <Icon />} </a> ) )} </div> </nav> </header> <style> ... </style>
- Refactor
src/components/Footer.astro
to use this configuration. The code insrc/components/Footer.astro
should look like this:--- import { socialMenu } from '@config/menus.config'; const today = new Date(); --- <footer> © {today.getFullYear()} Your name here. All rights reserved. <div class="social-links"> {socialMenu.map( ({icon: Icon, label, path}) => ( <a href={path} target="_blank"> <span class="sr-only">{label}</span> {Icon && <Icon />} </a> ) )} </div> </footer> <style> ... </style>
With the changes we just made:
- we can add tags to the head simply by modifying
src/components/Head.astro
(for tags that affect the entire website) or by using the namedslot
as “head” (for tags that affect a specific page or layout), - we can modify the menu elements simply by changing the content of the
src/config/menus.config.ts
file.
We can still do more to simplify future work… but that’s for another article.
A Little Trick
Thanks for making it this far. In upcoming articles, I’ll continue explaining how to create a custom template.
But if you don’t want to create one, you can use mine (the one you’ll have, more or less, if you follow all the steps in this series of articles) with the following command:
yarn create astro -- --template borjalofe/astro-blog-template