AstroJAM Stack

SERIES: Learn Astro while building Ink #3

Power your Astro site with Local and Remote Data

Aftab Alam

Aftab AlamSeptember 08, 2021

14 min read–––

Aftab Alam

You're on part 3 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!

3

Power your Astro site with Local and Remote Data

With the knowledge of Astro, its setup, styling, deployment as well as development approach, let's see how can we manage data with Astro in this article.

Use Local and Static data

The simplest form of data that we can feed in the Astro components can easily be .json files. If you see the current structure of the Nav.astro file, you can see that the component maintains its data which isn't ideal.

Let's created a typed structure to maintain the navigation data locally.

src/types.ts

export type NavItems = {
    [key: string]: NavItem
}

export type NavItem = {
    path: string
    title: string
}

We can maintain the site's navigation data in a separate *.ts file. Since the data is related to the site's configuration we'll call it a /src/config.ts file. Besides the navigation data, we can also use this file to maintain other site details like title, description, etc.

src/config.ts
import type{ NavItems } from './types'

export const NAV_ITEMS: NavItems = {
    home: {
        path: '/',
        title: 'home'
    },
    about: {
        path: '/about',
        title: 'about'
    }
}

// Site config
export const SITE = {
    // Your site's detail?
    name: 'Ink',
    title: 'Astro - Ink',
    description: 'Crisp, minimal, personal blog theme for Astro',
    url: 'https://astro-ink.vercel.app',
    githubUrl: 'https://github.com/one-aalam/astro-ink'
    // description ?
}

There's a good amount of components like Nav, Header, Footer, BaseHead, etc. that have data coupled with the mark-up. Let's go one by one and configure them to use the above configuration.

src/components/Nav.astro
---
import { toTitleCase } from '$/utils'
import { NAV_ITEMS } from '$/config'
---
<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>

SOURCE

  • Update the Header component
src/components/Header.astro
---
import { SITE } from '$/config'
import SvgIcon from './SvgIcon.astro' ---
<header class="header">
    <div class="header__logo">
        <a href="/" class="avatar">
            <img class="header__logo-img" src="/assets/logo.svg" alt="Astro logo" />
        </a>
    </div>
    <div class="header__meta flex-1">
        <h3 class="header__title">
<a href="">{ SITE.name }</a>
</h3> <div class="header__meta-more flex"> <p class="header__desc">
{ SITE.description }
</p> <nav class="header__nav flex"> <ul class="header__ref-list"> <li> <a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}> <SvgIcon> <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path> </SvgIcon> </a> </li> </ul> </nav> </div> </div> </header> <style> .header { @apply flex gap-4 border-b border-gray-200 dark:border-gray-700 py-3 } .header__logo-img { @apply w-16 h-16 rounded-full overflow-hidden } .header__title { @apply text-4xl font-extrabold md:text-5xl text-primarySecondary dark:text-primarySecondaryDark } .header__desc { @apply text-xl flex-1 dark:text-gray-200 } .header__ref-list { @apply flex gap-3 text-gray-400 } </style>

SOURCE

  • Update the Footer component
src/components/Footer.astro
---
import { SITE } from '$/config'
---
<div class="footer">
    <nav class="nav">
<div>2021 &copy; Copyright notice | <a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}>{ SITE.name }</a> theme on <a href="https://astro.build/">Astro</a></div>
</nav> </div> <style> .footer { @apply py-6 border-t dark:text-gray-300 border-gray-200 dark:border-gray-700 } </style>

SOURCE

  • Update the BaseHead component
/src/components/BaseHead.astro
---
import { SITE } from '$/config'
export type Props = { title: string description: string permalink: string image: string }
const { title = SITE.title , description, permalink, image } = Astro.props as Props
---

All of your site's common components are powered with locally configurable JSON data now, which can be easily re-configured as per your preference.

Markdown Front-Matter (local)

JSON is a lightweight data representation format that we can use to keep information that's global or very generic in nature. A content-based site is frequently authored, and something more flexible than JSON is needed to keep the content like blogs. Markdown *.md are the perfect, low-barrier syntax to author rich-text without much tooling overhead. It has all the constructs you're gonna need to format your content while composing them, that could easily be parsed and rendered into HTML by smart engines. Moreover, with markdown content we can maintain additional data in special blocks denoted by --- ... --- called Front-matter, which is perfect for maintaining post attributes like title, description, author, creationDate, etc. Smart markdown processing engines, can process markdown and react to the associated data and actual data differently - thus, giving you the opportunity to represent an entire blog, and data like author, creation through these files.

The code fence syntax --- ... --- you've already come across in the component article, seems inspired by this very ability of Markdown to have the associated data besides the actual content.

To have Markdown processed and rendered by Astro, we're gonna need some *.md, following this special structure - content and the metadata about the content a.k.a frontmatter.

Let's create a /src/pages/blog directory and put a bunch of markdown files following the format. You can copy the files from here too.

Prepare all the files with the following structure

---
title: Introducing Astro - Ship Less JavaScript
date: 2021-06-08
author: Fred K. Schott
authorTwitter: FredKSchott
category: design
tags:
- astro
- JAMStack
description: There's a simple secret to building a faster website — just ship less.
---

Unfortunately, modern web development has been trending in the opposite direction—towards more. More JavaScript, more features, more moving parts, and ultimately more complexity needed to keep it all running smoothly.

Today I'm excited to publicly share Astro: a new kind of static site builder that delivers lightning-fast performance with a modern developer experience. To design Astro, we borrowed the best parts of our favorite tools and then added a few innovations of our own, including:

- Bring Your Own Framework (BYOF): Build your site using React, Svelte, Vue, Preact, web components, or just plain ol' HTML + JavaScript.
- 100% Static HTML, No JS: Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.
- On-Demand Components: Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. If the user never sees it, they never load it.
- Fully-Featured: Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any of your favorite npm packages.
- SEO Enabled: Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication.

## Bring Your Own Framework

### Build your site using React, Svelte, Vue

> Need some JS? Astro can automatically hydrate interactive components...

[SEO Enabled](/some/relative/page/link)

This post marks the first public beta release of Astro. Missing features and bugs are still to be expected at this early stage. There are still some months to go before an official 1.0 release, but there are already several fast sites built with Astro in production today. We would love your early feedback as we move towards a v1.0 release later this year.

> To learn more about Astro and start building your first site, check out the project README.

Our posts markdown should act as a source of truth for all the data that belongs to a blog post or article.

Just creating a bunch of files, in the /blog directory, will auto-register the paths for static route generation i.e A page created at /pages/blog/intro-to-astro.md will make a path available at http://your-site-url.tld/blog/intro-to-astro and so on and so forth. You can keep any name for the folder, and Astro by convention will treat every *.md as it treats a *.astro file, and register a route. Amazing isn't it?

But, there's a catch. your markdown pages are all available, but your page looks glamor-less. Simple, boring, text without any visual personality. The head is also pretty empty. The mark-up needs a shell, or all the surrounding tags and style, and meta tags to make this page beautiful as well as findable. Enter layout file.

Layout for the Markdown

In the layout components section, you learned about the layout components, but if you see it from the implementation stand-point we're tapping into just half of its potential. The frontmatter area supports a specially recognized key by the Astro compiler layout, which lets us add something like

---
layout: $/layouts/post.astro
---

In the front matter. Astro will pick this layout, parse the actual content, and will feed it into a <Markdown> component we learned about earlier, and inject the processed markup in the <slot/> for your provided layout component. Besides that, it will also make all the frontmatter data available through a content Astro.props property, which can be accessed as

const { content } = Astro.props

Through content you'll have access to all the front-matter and can dynamically populate the layout template with the values from the post's front-matter, and can do all the amazing stuff like

<title>Astro - Ink {content.title && `| ${content.title}`}</title>
<meta name="description" content={content.description}/>

Since we don't have a post.astro layout component yet, let's create a separate layout file for the posts on the site. The full component should look like the following. We're creating a layout component with all the good ol *.astro` components.

---
import { SITE } from '$/config'
import MainLayout from '$/components/MainLayout.astro'
import BaseHead from '$/components/BaseHead.astro'
import Prose from '$/components/Prose.astro'

const { content } = Astro.props
---
<!doctype html>
<html lang="en">
    <head>
        <BaseHead title={ content.title ? `${ SITE.title } | ${content.title}` : SITE.title } description={ content.description }/>
    </head>
    <MainLayout>
        <div class="post__header">
            <h1 class="post__title">{ content.title }</h1>
            <h5 class="post__desc">
                <a class="post__author" href={`https://twitter.com/${content.authorTwitter}`} title={`${content.author}'s twitter`} target="_blank" rel="external">{ content.author }</a> |
                <span class="post__date">{ new Intl.DateTimeFormat('en-US', { dateStyle: 'full' }).format(new Date(content.date))}</span>
            </h5>
        </div>
        <!--<img src={content.image} alt={content.title} />-->
        <Prose>
            <slot />
        </Prose>
    </MainLayout>
</html>
<style>
    .post__header {
        @apply py-4 mb-1
    }
    .post__title {
        @apply text-5xl font-extrabold text-primary dark:text-primaryDark
    }
    .post__desc {
        @apply text-gray-500 dark:text-gray-100
    }
    .post__author {
        @apply no-underline dark:text-white
    }
    .post__date {
        @apply text-gray-400
    }
</style>

Re-visit the post URLs again and see the magic apply.

Other front-matter keys are user defined, but layout is specially known, and more interesting keys could land in the future.

Pull Local and Remote Data

We now have a working blog pipeline, with all the visual consistency we desired, but, you're still required to visit, and they'd not be findable without linking. Let's go ahead and link them

File-based path generation is auto-done for you, but only you could know

  • where you'd like to present the listing /blog, /post, /articles, /digital-garden
  • if you'd like to apply some filtering, sorting, etc. before presenting the listing

Due to the aforementioned reasons, the decision is left on you to build such pages and you're provided hooks to pull data from the site-local filesystem, as well as a remote location.

Astro.fetchContent Pull local data, and render listings

Astro ships with Astro.fetchContent that lets you access the files kept in the provided directory and return them in a parsed and very usable JSON format. The Astro global is available in all contexts in .astro files and fetchContent available in the Astro namespace can be used to read and retrieve a directory's file as you're calling an API. Taking the example of the posts we created in the earlier section you can scan all the content using

const posts = Astro.fetchContent('./post/*.md'); // location relative to where you're creating the listing file

.fetchContent() takes relative URL glob of the local .md files you’d like to import and returns an array of items of type:

{
   /** frontmatter from the post.. example frontmatter:
    title: '',
    date: '',
    image: '',
    author: '',
        authorTwitter: '',
    description: '',
   **/
    astro: {
      headers: [],
      source: '' // raw source of the markdown file
    },
    url: '' // the rendered path
  }[]

With this strcture, you could serve a blog index at the / by tweaking index.astro

/src/pages/index.astro
---
import DefaultPageLayout from '$/layouts/default.astro'
import PostPreviewList from '$/components/PostPreviewList.astro'
const title = 'Home' const description = 'Astro-Ink is a crisp, minimal, personal blog theme for Astro' const posts = Astro.fetchContent('./blog/*.md') ---
<DefaultPageLayout content={{ title, description }} showPageHeader={false}>
<PostPreviewList posts={posts} />
</DefaultPageLayout>

isting the posts, in a well-organized manner will require us to split the responsibility of listing the entire list of posts, and rendering individual post previews into separate components so let's go ahead and create the components.

  • For rendering individual post previews
/src/components/PostPreview.astro
---
const { post } = Astro.props
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', "Nov", 'Dec'] --- <div class="post-preview"> <div class="w-32"> <div class="post-preview__date">
<span class="post-preview__date__day">{ new Date(post.date).getDate() }</span>
<span class="post-preview__date__month-n-year">{ `${MONTHS[new Date(post.date).getMonth()]} ${new Date(post.date).getFullYear()}` }</span>
</div> </div> <div class="flex-1"> <h4 class="post-preview__title">
<a href={post.url} title={post.title}>{post.title}</a>
</h4> <p class="post-preview__desc">
{post.description}
</p> </div> </div> <style> .post-preview { @apply flex gap-6 } .post-preview__date { @apply flex flex-col w-full text-center } .post-preview__date__day { @apply text-6xl font-semibold text-gray-500 dark:text-gray-300 } .post-preview__date__month-n-year { @apply text-gray-400 } .post-preview__title { @apply text-2xl font-semibold text-primary dark:text-primaryDark hover:underline mb-2 } .post-preview__desc { @apply text-lg leading-6 dark:text-white line-clamp-2 } </style>

You're getting the post details and rendering them as needed with formatted date, title, description, and a link to their location.

For a listing, You'd take all the posts and loop over them to render PostPreview multiple times.

/src/components/PostPreviewList.astro
---
import PostPreview from './PostPreview.astro'
const { posts } = Astro.props ---
<section class="post-preview__list">
{posts.map((post) => (
<PostPreview post={post}/>
))} </section> <style> .post-preview__list { @apply flex flex-col gap-12 } </style>

With the above changes, you'll now have a listing to all the posts you're maintaining at /.

SOURCE

Few More Enhancements

Create a dedicated /blog page

The homepage of your site can have multiple sections, so we can keep a /blog section on our site, dedicated to all things blog.

src/pages/blog.astro
---
import DefaultPageLayout from '$/layouts/default.astro'
import PostPreviewList from '$/components/PostPreviewList.astro'

let title = 'Blog';
let description = 'All the articles posted so far...'

const posts = Astro.fetchContent('./blog/*.md')
---
<DefaultPageLayout content={{ title, description }}>
    <PostPreviewList posts={posts} />
</DefaultPageLayout>
/src/config.ts
import type{ NavItems } from './types'

export const NAV_ITEMS: NavItems = {
    home: {
        path: '/',
        title: 'home'
    },
    blog: { // *new entry for the dedicated blog section
        path: '/blog',
        title: 'blog'
    },
    about: {
        path: '/about',
        title: 'about'
    }
}
// previous code unchanged
src/pages/index.astro
---
import DefaultPageLayout from '$/layouts/default.astro'
import PostPreviewList from '$/components/PostPreviewList.astro'

const title = 'Home'
const description = 'Astro-Ink is a crisp, minimal, personal blog theme for Astro'

const posts = Astro.fetchContent('./blog/*.md')
---
<DefaultPageLayout content={{ title, description }} showPageHeader={false}>
    <PostPreviewList posts={posts} />
    <div class="page__actions">
<a class="action__go-to-blog" href="/blog" title="All Posts">All Posts &rarr;</a>
</div> </DefaultPageLayout> <style> .page__actions { @apply flex justify-center md:justify-end py-6 } .action__go-to-blog { @apply text-base uppercase text-gray-400 dark:text-gray-600 hover:underline } </style>

Commit

Remote

Astro.fetchContent will take care of preparing JSON data from all your local markdown files but what to do if it's remote data you want to consume?

Nothing special or new, you can just use native fetch

Let's see that with the help of an example.

How about showing all the interesting videos/screencasts on Astro on the site?

Add and commit the following data in src/data/astro-media.json Commit

lets's add a new Video and Screencasts page to host the data.

Commit We'll create a MediaPreview.astro and MediaPreviewList.astro component, like BlogPreview and BlogPreviewList with a slight customization, and for a better control like here.

Add media path in the global nav in the site config /src/config.ts

    media: {
        path: '/media',
        title: 'media'
    },

And put the following code in the dedicated media page at /src/pages/media.astro

/src/pages/media.astro
---
import DefaultPageLayout from '$/layouts/default.astro'
import MediaPreviewList from '$/components/MediaPreviewList.astro'
// import posts from '$/data/astro-media.json'

let title = 'Videos & Screencasts';
let description = 'All the great videos on Astro we could find for ya!'

const response = await fetch('https://raw.githubusercontent.com/one-aalam/astro-ink/main/src/data/astro-media.json')
const posts = await response.json()
---
<DefaultPageLayout content={{ title, description }}>
    <MediaPreviewList posts={posts} />
</DefaultPageLayout>

Though we have used Github raw data, you can use all the external APIs here.

Conclusion

We now have all the understanding to create pages, and consume data from local as well as remote sources. We're in a good position to create all sorts of static sites. But, modern sites are not just static, they have interactivity too. How will you add something interactive? No, worries - Astro has got you covered there. Astro has out-of-the-box support for multiple front-end frameworks/libraries like React, Vue, Svelte, etc.

Let's cover the interactivity and more in the upcoming articles.

Thank you for being till the end 🙌 . If you enjoyed this article, or learned something new, support me by clicking the share button below to reach more people and/or give me a follow on Twitter to see some other tips, articles, and things I learn and share there.

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