How I used Astro to build this website
This is my first blog post, it describes several fundamental concepts I learned and used to build this website using Astro and TailwindCSS, such as Astro layouts and components, markdown styling, and variables in Astro.
I chose to build this site with Astro and TailwindCSS for several reasons. Astro is an excellent framework to build a portfolio website with and is a joy to work with thanks to it’s rich documentation and intuitive structure. It allows for progressive enhancement, gradually adding new and more complex features to the site as I learn more about web development and Astro’s features.
TailwindCSS provides a well thought out design system that speeds up development by abstracting CSS classes into utility classes designed to be used in conjunction with each other. This promotes a consistent design system and speeds up development.
Layouts
In Astro, layouts are used to create a base structure for pages which can include the websites main components and styling like navbars, footers, and the main color scheme. Layouts can also be used to import other layouts, allowing for a nested layout structure; for example, a base layout containing HTML boilerplate and SEO, and a markdown layout which provides styling and compoents for blog posts and articles.
Base Layout
I first created a BaseLayout.astro
file to set up the basic structure of my pages. This layout includes the HTML boilerplate, head section, my navbar and footer components, and a <slot />
tag where the page’s content is inserted. A named <slot />
tag is also used in the head section to allow for the insertion of additional page specific head elements like schema data and meta tags.
In this layout I’m using Astro.props
to pass the title and description of the page to the layout. This allows the these fields to be set dynamically based on each page’s content.
Main Layout
I then created a MainLayout.astro
file which imports my base layout, defines the schema data for my website’s main pages, and passes metadata like the title and description to the base layout’s meta tags.
To pass page specific metadata to the base layout, I use the spread operator with Astro.props
allowing all properties defined on the main layout component to be available to the base layout.
On each page, the title and description props are defined on the main layout component, and are then passed to the base layout.
Markdown Layout
I also created a MDLayout.astro
file to define the styling and structure for blog posts. This layout is a lot more complex than the main layout, it includes styling for markdown content, a table of contents, post specific schema data, and image handling.
Frontmatter and Astro.props
Frontmatter variables defined in markdown files make post specific metadata available to the Astro.props
object: const { frontmatter } = Astro.props;
This allows these values to be accessed by the base layout’s {description}
and {title}
variables by spreading {...frontmatter}
into it.
SEO Schema
I created a JSON-LD object to define the schema data for each blog post. This object uses values from the frontmatter to set the headline, description, and date published from each blog post and includes the post’s image if it exists.
The published date is formatted to be compatible with the ISO 8601 standard to comply with Google’s schema specifications and the image URL is created for locally hosted images using the new URL()
method to provide a full image URL to the schema based on the current page’s URL.
Image Optimization
Images are optimized using the Astro Image
component. In order to optimize each post’s images dynamically, they need to be imported into the layout file’s Image
component. I used the import.meta.glob
function to selectively import a post’s image from the /src/images
directory by filtering through them using the frontmatter.image
value, then imported the image using {images[frontmatter.image]()}
.
Importing Layouts
Layouts are imported differently depending on the file type.
Markdown Files
Astro Files
Markdown styling
TailwindCSS resets default browser styles so all HTML elements rendered from markdown look like plain text. To style markdown, I used the official @tailwindcss/typography plugin which provides well thought out, opinionated markdown styling.
Prose is the main utility class used to style markdown. There are a wide range of prose modifiers that can be used to change the look of the content such as prose-sm
to make the text smaller or prose-lg
to make it larger and modifiers to target each element type like prose-h1
or prose-headings
to target all headings. In addition prose-invert
changes the default color of all text to white as opposed to.
Syntax Highlighting
By default Astro will highlight any code in markdown files. Changing the code syntax highlighting theme in Astro is easy, I just needed to add a shikiConfig object to the astro.config.mjs file and set the desired theme.
Card Component
To display content, I created a customizable Card
component. This component accepts a variety of props which can be used to configure the component for the page it’s being imported into.
The Card
also conditionally renders title, subtitle, and heading if they are provided to the component. If not, only content passed to the slot
is rendered within the Card
.
There are also variable styles which can be defined based on the props passed. For example, padding
sets a default padding value to card-p
, a custom TailwindCSS @layer component
I created to define card padding. The padding
prop can also be set to a custom value when the component is called in a file, overriding the default value.
The variant
prop defines whether or not the Card
has a border, it’s two values being bordered and borderless. When this prop isn’t set, the Card
defaults to bordered.
noMargin
removes the component’s default mx-4
margins. I use this to remove margins for smaller Card
components like my Projects
component.
displayHr
displays a horizontal divider between the title, subtitle and main card content.
And the class
prop allows me to pass any custom classes I want when calling this component in a file.
The Card
uses a <slot>
to insert any type or amount of HTML elements when the component is called in a file.
When inserting new elements, name="content"
is defined on them to identify all elements to be rendered within the <slot>
element. A single wrapper div can also be defined with name="content"
and any elements wrapped by that div will be slotted into the Card
component.
Using the Card
component in pages
This example shows how I implemented the <Card />
component on my index.astro
page. The component is imported in the frontmatter with import Card from '@components/Card.astro';
and is used to wrap the main content on my page. In this page I set the variant
prop to borderless
, passed a title and subtitle, and created a couple <p>
elements with slot="content"
so they’re properly slotted into the Card
component.
Projects component
I created a Projects
component to display web development projects I’ve worked on. In this component I used the Card
component within a function that maps over and displays each project in an unordered list. I customized the card for this component by setting it to bordered
, with a custom padding value passed to the padding
prop, and noMargin
set to true
Using Astro’s Content Collections
My projects utilize Astro’s content collections, each project is defined as a single YAML
file within a content/projects
directory and are made available to the Projects
component with const projects = await getCollection('projects');
Now, when I want to add a new project, all I have to do is create a new YAML
file with my project’s information in /projects
and it will automatically be rendered within a card in the Projects
component.