AstroJAM Stack

SERIES: Learn Astro while building Ink #2

Manage your Astro Site like a Pro

Aftab Alam

Aftab AlamSeptember 07, 2021

16 min read–––

Aftab Alam

You're on part 2 of the 5-part series Learn Astro while building Ink where you'll build a blog(Ink) using astro.build, one of the most promising static/JAM Stack site builder today!

2

Manage your Astro Site like a Pro

In the previous article you got to meet Astro, one of the most promising SSG today. You got to know about a few of its benefits, how it could be set up, integrated with TailwindCSS, and then deployed to Vercel. From this article onwards, we'll dive into different aspects of an Astro site, and we'll start with page components.

Currently, we have just one index.astro page component. If we need more pages we can add more *.astro files in the /pages directory like about.astro for /about, or blog.astro for /blog. With the growing number of pages, it could soon be required to re-use common parts of the page like header, footer, nav, etc. for maintainability and DRYness. *.astro components placed inside the /components directory, are used as a convention to keep such re-usable components.

Understanding Astro Components

Astro Components are how you split up responsibility into more re-usable and manageable chunks. Akin to other modern Frameworks, .astro is a specially recognized component extension/format to Astro. *.astro files are like .jsx, .vue, or .svelte from the DX standpoint,

  • as you get to compose bigger, compound components from smaller, single-purpose components, to prepare the whole page eventually,
  • and, you get to encapsulate mark-up, behavior, and style in a single component

but they are also like the server-side templating language like nunjucks, pug, ejs, handlebars, etc. from the functional standpoint, as

  • they're meant to eventually produce purely HTML
  • interactivity/hydration is opt-in

Astro Components - The Syntax

---
    // component's code
---
<!-- HTML -->
<script>
    // client side code
</script>
<style>
    /* style */
</style>

The area between the code fence --- ... --- is your portal to access the modern Javascript/Typescript features

  • define variables
  • export variables that can be filled in by the user of your .astro files
  • import ES6 based common, re-usable code(including components) from your codebase or the NPM ecosystem
  • define locally invokable functions, that can be called in the template

It's like a little window to full-ES capabilities, that's run at the build time

As discussed previously, components are how you split up responsibility into more re-usable and manageable chunks. Akin to other modern Frameworks, .astro is a specially recognized component extension/syntax format to Astro, just like .jsx, .vue, or .svelte that we can insert into other, high-order components to prepare the whole page eventually. Since this format is unique to Astro - Astro uses .astro to introduce a few cool features and niceties, unlike the other client-side FE frameworks that you use. In their words

Astro introduces a special .astro format, which combines the best of HTML with the best of JavaScript.

Let's understand the best parts.

The best of HTML

Since, with Astro you want to build .html files, that could be hosted statically on cloud solutions like Vercel, Netlify, etc. Astro considers .html as the lingua-franca for your components, and natively supports markdown(.md) with no distinction between a .astro component and a .md page in how they're treated.

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <main>
      <h1>Hello world!</h1>
    </main>
  </body>
</html>

and

<main>
  <h1>Hello world!</h1>
</main>

A component-x.astro is a valid Astro component, as well as a markdown page inside /pages/ is a valid Astro component too. I find this concept very much like template partials available in many BE side templating languages like handlebars, nunjucks, etc. conceptually. This mark-up is pre-rendered into HTML when you do an astro build.

Markdown syntax(and MDX) is the ultimate pre-processor syntax for HTML, due to its easy-to-remember dialects and myriad of tools that can process it cheaply. Considering that, Astro does not stop there and it adds few more nice things. Markdown support is provided natively in Astro through marked, plus it supports and extends the front-matter syntax.

The best of JS

ES Imports

You can have ES Module imports for components, and common code/logic

---
import { Markdown } from 'astro/components';
import AstroComponentX from '../components/astro-component-x';
---

and then can mix your markup with these components/logic, like the example you get in generic template

<body>
    <main>
        <header>
            <div>
                <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
                <h1>Welcome to <a href="https://astro.build/">Astro</a></h1>
            </div>
        </header>

        <Tour />
    </main>
</body>

Local data/variables

You can define local variables

---
const greeting = 'Hello';
let name = 'world';
let items = ['item1', 'item2', 'item3']
---

and then use their values with a {var}

<main>
    <h1>Hello {name}!</h1>
</main>

and loop over with a map

<main>
  <h1>Hello {name}!</h1>
  <h2 data-hint={`Use JS template strings when you need to mix-in ${"variables"}.`}>So good!</h2>
  <ul>
    {items.map((item) => (
      <li>{item}</li>
    ))}
  </ul>
</main>

Let's see how we can apply the above concepts to structure our site.

There are parts in the index.astro like the meta-tags we have in the <head> that's common, and many pages will eventually rely upon.

/src/components/BaseHead.astro
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap" rel="stylesheet">

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>Astro Ink</title>
<meta name="title" content="Astro Ink"/>
<meta name="description" content="Crisp, minimal, personal blog theme for Astro"/>

<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="shortcut icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">

<meta httpEquiv="X-UA-Compatible" content="IE=edge"  />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="msapplication-config" content="/browserconfig.xml"/>
<meta name="theme-color" content="#ffffff" />
<!-- Link to the global style, or the file that imports TailwindCSS constructs -->
<link rel="stylesheet" href="/style/global.css">
/src/components/Intro.astro
<img class="w-1/3" src="/assets/yay.svg" alt="Yay!" />
<h1 class="h1">Hello, World!</h1>

<style>
    img {
        @apply mx-auto mt-6
    }
    h1 {
      @apply w-full justify-center text-center text-3xl font-bold text-purple-600 py-10
    }
</style>

Once re-factored, the index.astro page could simply used as

src/pages/index.astro
---
import BaseHead from '$/components/BaseHead.astro'
import Intro from '$/components/Intro.astro'
---
<html>
  <head>
    <BaseHead />
  </head>
  <body class="font-sans antialiased">
    <Intro/>
  </body>
</html>

Neat!

Pass props to Astro components

Components can't be of much use without the ability to receive data as properties that they could use to render different data sets with the same mark-up. Astro.props within the code fence --- /* code */ --- is how you receive dynamic properties from a parent Astro component or page.

For instance, The BaseHead.astro component we created earlier has properties like title, description, etc. that a calling page component would like to control. Astro.props help us here, and in similar scenarios to let us pull the provided properties, and render them conditionally or unconditionally.

The BaseHead.astro component could now be adapted as

---
    const { title = 'Astro - Ink', description, permalink, image } = Astro.props
---
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>{title}</title>
    <meta name="title" content={title}/>
{ description &&
<meta name="description" content={description}/>
}
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="shortcut icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="manifest" href="/site.webmanifest"> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> <meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-config" content="/browserconfig.xml"/> <meta name="theme-color" content="#ffffff" /> <!-- Open Graph Tags (Facebook) --> <meta property="og:type" content="website" /> <meta property="og:title" content={title} />
{permalink &&
<meta property="og:url" content={permalink} />
}
{description &&
<meta property="og:description" content={description} />
}
{image &&
<meta property="og:image" content={image} />
}
<!-- Twitter --> <meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:title" content={title} /> {permalink && <meta property="twitter:url" content={permalink} /> } {description && <meta property="twitter:description" content={description} /> } {image && <meta property="twitter:image" content={image} /> } <!-- Link to the global style, or the file that imports TailwindCSS constructs --> <link rel="stylesheet" href="/style/global.css">

As you can see, there are tags like og:(opengraph) and twitter: that could lead to empty values/empty markup so we're skipping rendering them altogether.

Since Astro supports typescript natively, you can also do

    export type Props = {
        title: string
        description: string
        permalink: string
        image: string
    }

    const { title = 'Astro - Ink', description, permalink, image } = Astro.props as Props

do remember to create a .tsconfig.json file as shown here, and install typescript as a dev dependency for this to work

With the above changes, the index.astro file can now be adapted to

src/pages/index.astro
---
import BaseHead from '$/components/BaseHead.astro'
import Intro from '$/components/Intro.astro'
---
<html>
  <head>
<BaseHead title="Astro Ink | Home" />
</head> <body class="font-sans antialiased"> <Intro/> </body> </html>

Note: It's similar to how we pass props in React, Svelte, etc.

Components within Components, with slot

Astro.props can help us consume Javascript-based primitive data types and structures, but that's not all a component can consume. We also are required to re-use many of the component visual characteristics, or behavior many times. How could we facilitate the same with Astro? The answer to it is <slot/>!

<slot/> let you place any component in-place, within another component, just like you work naturally in HTML - <strong/> inside a <p/>, or an <input/> field inside a <form/>. <slot/> promote composition, so that you could build the ultimate page while composing small, independent, single-purpose components.

For us, <body/> ultimate wraps everything in a page, the BaseLayout component is one of the perfect candidates to use <slot/> so that other components that use BaseLayout can decide what they wanna put while retaining the <body/> structure.

src/components/BaseLayout.astro here

src/components/BaseLayout.astro
<body class="font-sans antialiased bg-gray-100 dark:bg-gray-800 transition-colors">
        <main class="mx-auto max-w-4xl px-4 md:px-0">
            <slot />
        </main>
</body>

With this change, the /pages/index.astro can further be simplified to /pages/index.astro here

src/pages/index.astro
---
import BaseHead from '$/components/BaseHead.astro'
import BaseLayout from '$/components/BaseLayout.astro' import Intro from '$/components/Intro.astro' ---
<html>
  <head>
    <BaseHead title="Astro Ink | Home" />
  </head>
<BaseLayout>
<Intro/>
</BaseLayout>
</html>

As an exercise, follow this and create header, footer and a MainLayout.astro, etc.

The BaseLayout itslef can be used to build more high-order, layout-focussed components.

src/components/MainLayout.astro
---
import BaseLayout from './BaseLayout.astro';
import Header from './Header.astro';
import Footer from './Footer.astro';
---
<BaseLayout>
    <br class="my-4"/>
    <Header/>
    <div class="content">
        <slot />
    </div>
    <br class="my-4"/>
    <Footer/>
</BaseLayout>

<style>
    .content {
        min-height: 580px
    }
</style>

And we can improve, the index page even further

src/pages/index.astro
---
import BaseHead from '$/components/BaseHead.astro'
import MainLayout from '$/components/MainLayout.astro' import Intro from '$/components/Intro.astro' ---
<html>
  <head>
    <BaseHead title="Astro Ink | Home" />
  </head>
<MainLayout>
<Intro/>
</MainLayout>
</html>

Multiple Pages + Linking

With all the re-usability, compositon concerns managed for one page, we can easiliy extend it to other pages, and the entire site eventually.

Let's create an about page.

src/pages/about.astro
---
import BaseHead from '$/components/BaseHead.astro'
import MainLayout from '$/components/MainLayout.astro'
const title = 'About';
const description = 'There\'s a simple secret to building a faster website — just ship less.';
---
<html>
  <head>
    <BaseHead title={`Astro Ink | ${title}`} description={description} />
  </head>
  <MainLayout>
    <div class="page__header mb-6">
        <h1 class="page__title">{title}</h1>
        <h5 class="page__desc">{description}</h5>
    </div>

    <article class="page__content prose lg:prose-md max-w-none dark:text-white">
        <p>
            Astro-Ink is a crisp, minimal, personal blog theme for Astro, that shows the capability of statically built sites - offering all the goodness and DX of the modern JS ecosystem without actually shipping any JS by default. It's built by...
        </p>
        <h3>Few Bots, Meta-humans & a Guy!</h3>
        <div class="author">
            <img class="rounded-full" width="160" src="https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358878e2493fbea064dd9_peep-59.svg" title="Aalam" />
            <div>
                Aftab Alam //
                <a href="https://twitter.com/aftabbuddy" title="Author's Twitter Handle">@aftabbuddy</a> //
                <a href="https://github.com/one-aalam" title="Author's Github URL">one-aalam</a>
            </div>
        </div>
    </article>
  </MainLayout>
</html>

<style>
    .page__header {
        @apply py-4 mb-1
    }
    .page__title {
        @apply text-5xl font-extrabold text-primary dark:text-primaryDark
    }
    .page__desc {
        @apply text-gray-400
    }
    .author {
        @apply flex flex-col w-full
    }
</style>

It could be laborious to remember and type these sections of the site, so we'll introduce a navigation component too.

src/components/Nav.astro
---
import { toTitleCase } from '$/utils'

const NAV_ITEMS: NavItems = {
    home: {
        path: '/',
        title: 'home'
    },
    about: {
        path: '/about',
        title: 'about'
    }
}
---
<nav class="nav py-3">
    <ul class="nav-list">
         {
            Object.keys(NAV_ITEMS).map(navItemKey => <li>
                <a class="hover:underline" href={NAV_ITEMS[navItemKey].path} title={NAV_ITEMS[navItemKey].title}>{toTitleCase(NAV_ITEMS[navItemKey].title)}</a>
            </li>)
        }
    </ul>
</nav>
<style>
    .nav-list {
        @apply inline-flex list-none gap-8 text-xl font-semibold text-primarySecondary dark:text-primarySecondaryDark py-2
    }
</style>

We can keep general purpose utilities like toTitleCase in a src/utils/index.ts directory

src/utils/index.ts
export const toTitleCase = (str: string) => str.replace(
      /\w\S*/g,
      function(txt) {
        return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
      }
    )

Once done, we can link the Nav component to the MainLayout

src/components/MainLayout.astro
---
import BaseLayout from './BaseLayout.astro';
import Header from './Header.astro';
import Footer from './Footer.astro';
import Nav from './Nav.astro'; ---
<BaseLayout>
    <br class="my-4"/>
    <Header/>
<Nav/>
<div class="content"> <slot /> </div> <br class="my-4"/> <Footer/> </BaseLayout>

Add Layout Components

The components directory contains all the re-usable components now. We can keep all sorts of components there, but, based on what they do and are used for, you'd see that some components are atomic, and used for single-purpose, and for composing the whole page, whereas others act as a template, where they decide the entire layout of a single page and share the similar visual characteristics. This distinction is important from an organization's point of view and helps you centrally manage how several parts of your site could look. Examples are your top-level pages like home, about, blog - having similar headers for visual consistency, and all of your articles having a bit tweaked/different layout.

We can keep a /layout directory to keep all such components. MainLayout/BaseLayout components can just worry about common wrapper markup for head and body, whereas the component in the layout directory can take more specific visual duties.

astro-ink -39c2d60d538b7c7c642a82e6ef781b50d9fc6973

feat(2.6): Add common layout components

src/layouts/default.astro
---
import BaseHead from '../components/BaseHead.astro';
import MainLayout from '../components/MainLayout.astro';

const { content, showPageHeader = true } = Astro.props
---
<!doctype html>
<html lang="en">
    <head>
        <BaseHead title={content.title ? `Astro - Ink | ${content.title}` : ``} description={content.description}/>
    </head>
    <MainLayout>
        {showPageHeader &&
            <div class="page__header">
                <h1 class="page__title">{content.title}</h1>
                <h5 class="page__desc">{content.description}</h5>
            </div>
        }
        <slot />
    </MainLayout>
</html>
<style>
    .page__header {
        @apply py-4 mb-1
    }
    .page__title {
        @apply text-5xl font-extrabold text-primary dark:text-primaryDark
    }
    .page__desc {
        @apply text-gray-400
    }
</style>
/src/pages/index.astro
---
import DefaultPageLayout from '$/layouts/default.astro' import Intro from '$/components/Intro.astro' const title = 'Home' const description = 'Astro-Ink is a crisp, minimal, personal blog theme for Astro' ---
<DefaultPageLayout content={{ title, description }} showPageHeader={false}>
<Intro/>
</DefaultPageLayout>

SOURCE

/src/pages/about.astro
---
import DefaultPageLayout from '$/layouts/default.astro' const title = 'About'; const description = 'There\'s a simple secret to building a faster website — just ship less.'; ---
<DefaultPageLayout content={{ title, description }}>
<article class="page__content prose lg:prose-md max-w-none dark:text-white"> <p> Astro-Ink is a crisp, minimal, personal blog theme for Astro, that shows the capability of statically built sites - offering all the goodness and DX of the modern JS ecosystem without actually shipping any JS by default. It's built by... </p> <h3>Few Bots, Meta-humans & a Guy!</h3> <div class="author"> <img class="rounded-full" width="160" src="https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358878e2493fbea064dd9_peep-59.svg" title="Aalam" /> <div> Aftab Alam // <a href="https://twitter.com/aftabbuddy" title="Author's Twitter Handle">@aftabbuddy</a> // <a href="https://github.com/one-aalam" title="Author's Github URL">one-aalam</a> </div> </div> </article> </DefaultPageLayout>
<style> .author { @apply flex flex-col w-full } </style>

SOURCE

Astro's default components

Blog, Documentation, etc. are the category of sites that are more suitable for pre-generated static builds, and markdown is the lingua franca that lends itself pretty well to simple, approachable, and convenient content authoring. Wouldn't it be good if you won't have to rely on external/community plugins to do such essential and commonly needed jobs?

Astro ships with components for Markdown and syntax highlighting by default.

Markdown

Astro has built-in support for Markdown. You can effortlessly mix markdown with HTML by enclosing markdown in a <Markdown> component, and put them anywhere in your Astro component, to render valid and pure HTML.

astro-ink -2aaed40264cefad0dabfac410bca0f964b27d6a0

feat(2.7): Add Markdown component

src/pages/about.astro
---
import Markdown from 'astro/components'
import DefaultPageLayout from '$/layouts/default.astro'

const title = 'About';
const description = 'There\'s a simple secret to building a faster website — just ship less.';
---
<DefaultPageLayout content={{ title, description }}>
    <article class="page__content prose lg:prose-md max-w-none dark:text-white">
        <Markdown>
            Astro-Ink is a crisp, minimal, personal blog theme for Astro, that shows the capability of statically built sites - offering all the goodness and DX of the modern JS ecosystem without actually shipping any JS by default. It's built by...

            ## Few Bots, Meta-humans & a Guy!
            <div class="author">
                <img class="rounded-full" width="160" src="https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358878e2493fbea064dd9_peep-59.svg" title="Aalam" />
                <div>
                    Aftab Alam //
                    [@aftabbuddy](https://twitter.com/aftabbuddy)  //
                    [one-aalam](https://github.com/one-aalam)
                </div>
            </div>
        </Markdown>
    </article>
</DefaultPageLayout>

<style>
    .author {
        @apply flex flex-col w-full
    }
</style>

The markdown component sits at the center of Astro's markdown experience, and we'll dive more into it in the upcoming articles.

The following markdown

This is bunch of articles we've posted and linked so far about the island architecture, SPA fatigue and Astro.

[Introducing Astro](post/introducing-astro)
[Second-guessing the modern web](/post/spa-fatigue)
[Islands Architecture](/post/islands-architecture)

can be expressed as

---
import Markdown from 'astro/components';
import Page from '../layouts/page.astro';

let title = 'Blog'
let description = 'All the articles posted so far...';
---
<Page content={{ title, description }}>
    <Markdown>
        This is bunch of articles we've posted and linked so far about the island architecture, SPA fatigue and Astro.

        [Introducing Astro](post/introducing-astro)
        [Second-guessing the modern web](/post/spa-fatigue)
        [Islands Architecture](/post/islands-architecture)
    </Markdown>
</Page>

The markdown <Markdown> component can use other .astro components, like any other .astro file and our pages could be further simplified.

<Layout>
  <Markdown content={content} />
</Layout>

Syntax highlighting

Astro supports syntax highlighting too...and just like <Markdown/> you can import prism, to have your code highlighted

Re-factoring = Add Prose component

All of our body types inherit the same typographic rhythm and chacrterstics, let's create a Prose component too. We'll use slot, so that it can easily be used as a wrapper component.

src/components/Prose.astro
<article class="prose">
    <slot />
</article>
<style>
    .prose {
        @apply max-w-none dark:text-white
        /* Size Modifiers: https://github.com/tailwindlabs/tailwindcss-typography#size-modifiers */
        /* Color Themes: https://github.com/tailwindlabs/tailwindcss-typography#color-modifiers */
    }
</style>
astro-ink -0470595b7cc4eec80b24a2fddb5695d1da5f096b

re-factor(2): Add Prose component

/src/pages/about.astro
---
import Markdown from 'astro/components'
import DefaultPageLayout from '$/layouts/default.astro'
import Prose from '$/components/Prose.astro'
const title = 'About'; const description = 'There\'s a simple secret to building a faster website — just ship less.'; ---
<DefaultPageLayout content={{ title, description }}>
<Prose>
<Markdown> Astro-Ink is a crisp, minimal, personal blog theme for Astro, that shows the capability of statically built sites - offering all the goodness and DX of the modern JS ecosystem without actually shipping any JS by default. It's built by... ## Few Bots, Meta-humans & a Guy! <div class="author"> <img class="rounded-full" width="160" src="https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358878e2493fbea064dd9_peep-59.svg" title="Aalam" /> <div> Aftab Alam // [@aftabbuddy](https://twitter.com/aftabbuddy) // [one-aalam](https://github.com/one-aalam) </div> </div> </Markdown>
</Prose>
</DefaultPageLayout> <style> .author { @apply flex flex-col w-full } </style>

References

  • Astro versus JSX
  • Astro's .astro are pretty new extension format so the IDE support is not that great but if you're a VS Code user there are few extensions you can try!

Conclusion

We explored the *.astro static rendering features in this article and understood

  • What are *.astro components and how they work
  • How component can help us build pages
  • Patterns and best practices to manage sites as well as components

There's more to the component story, as *.astro is a multi-framework framework, and just as you've rendered *.astro files statically, you can render a *.svelte, *.vue, or *.jsx file too, with interactivity. But, before we visit this other side of the component story, let's see how we can pull data in an Astro site in the next article.

Thank you for being till the end 🙌 . If you enjoyed this article, or learned something new, please take a second to tweet this article or share on LinkedIn so others can discover it. Or add to the discussion on Twitter.

Tweet about this post(by clicking the button above) or like/re-tweet one of the tweets that shares this article, and it will show up here eventually