AstroJAM Stack

SERIES: Learn Astro while building Ink #5

Build Dynamic, Data-driven Pages with Astro

Aftab Alam

Aftab AlamSeptember 10, 2021

9 min read–––

Aftab Alam

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

5

Build Dynamic, Data-driven Pages with Astro

In the previous articles you discovered, how you can manage pages in an Astro site and feed it with both local and remote data. You also learned how interactivity could be sprinkled over purely static HTML through mindful constructs. But manually creating the pages and populating them with data later at the build time can only appeal to and accommodate for a segment of sites where all the pages can be known in advance. Realistically, pages are not just data dependant, they're data-driven or controlled. Even from a UX standpoint, presenting more data than immediately a user may need concerns UX and findability of information ie. blog index page. With a sufficiently large or content-based site, you'd also be needed to classify the content and present it in an easily findable and navigable manner i.e. blog category or tag pages.

These requirements compel for a need to be able to create pages programmatically. The ability to create more pages than what a static file-system-based page generation may offer.

With the right tools provided, You can do things such as

  • Group content based on metadata available in local or remote content - like /category, /tag, /author
  • create pages programmatically, and support pagination

Let's learn about the dynamic pages by extending our site to support tags.

Create a Tags page

Our site is currently missing a way to look up posts on tags. We might not even have a tag associated with the posts. To support the tag-based pages, let's first add a tags key to the front-matter of all the markdown posts.

---
tags:
- JAMStack
- Astro
---

Once done, let's create a dedicated tags page at src/pages/tags/index.astro (/tags route )

src/pages/tags/index.astro
---
import DefaultPageLayout from '$/layouts/default.astro'

let title = 'All Tags'
let description = 'All the tags used so far...'

// fetch all the posts
const allPosts = await Astro.fetchContent('../blog/*.md')

// prepare an array with all the unique tags
const tags = [...new Set([].concat.apply([], allPosts.map(post => post.tags)))]
---
<DefaultPageLayout content={{ title, description }}>
    <ul class="tag-list">
     {tags.map((tag) => (
        <li><a class="tag" href={`/tags/${tag}`} title={`View posts tagged under "${tag}"`}>{tag}</a></li>
    ))}
    </ul>
</DefaultPageLayout>
<style>
    .tag-list {
        @apply list-none flex gap-2
    }
    .tag {
        @apply inline-block text-xl px-4 py-1 rounded-full text-primary bg-primaryDark dark:bg-primary dark:text-primaryDark hover:bg-primary hover:text-primaryDark dark:hover:bg-primaryDark dark:hover:text-primary
    }
</style>

That's all the tags available with all the posts on the site, indexed at /tags

Meet the Dyanmic pages and getStaticPaths

All the tags at /tags are referring to URLs of the form /tags/${tag} -> /tags/JAMStack, /tags/astro but we don't have any pages by those names yet. Creating individual pages by all the tags path wouldn't be a smart, satisfactory, or efficient process, so Astro exposes a getStaticPaths method you could use to generate the pages on-demand. getStaticPaths imposes a structural expectation by adhering to which, you can hook into the Astro's page generation process, and inform of all the URLs to prepare, alongside their content, as you've created the pages manually. Cool, isn't it?

Here's how you can use it

export async function getStaticPaths({}) {
  const allPosts = Astro.fetchContent('../../blog/*.md')
  const allTags = new Set()
  allPosts.map(post => {
      post.tags && post.tags.map(tag => allTags.add(tag.toLowerCase()))
  })

  return Array.from(allTags).map((tag) => {
    // All the posts that match the current `tag`
    const filteredPosts = allPosts.filter((post) => post.tags.includes(tag))
    return {
      // return structure format
    };
  });
}

But how would you retrieve the current tag or the dynamic parts from a URL?

Simple! You'll have to provide it. The return structure in getStaticPaths should be of the form -

params: { tag },
props: {
    pages: filteredPosts
}

where params signify the dynamic parts of the URL, /tags/${tag}. For every tag you'll have a URL, that Astro treats just like a static path - /tags/${tag} -> /tags/JAMStack, /tags/astro. props are the properties that you want to make available to the component. The most important property for us is the posts filtered by a tag which we're passing to pages. The resultant code inside --- /* */ --- should look like.

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

export async function getStaticPaths({ }) {
  const allPosts = Astro.fetchContent('../../blog/*.md')
  const allTags = new Set()
  allPosts.map(post => {
      post.tags && post.tags.map(tag => allTags.add(tag.toLowerCase()))
  })

  return Array.from(allTags).map((tag) => {
    const filteredPosts = allPosts.filter((post) => post.tags.includes(tag))
    return {
      params: { tag },
      props: {
          pages: filteredPosts
      }
    };
  });
}

const { pages } = Astro.props // pages prop passed in `getStaticPaths`
const { params } = Astro.request // `params` passed in `getStaticPaths`
---
<DefaultPageLayout content={{ title: `Posts by Tag: ${params.tag}`, description: `all of the articles we've posted and linked so far under the tag: ${params.tag}` }}>
    <PostPreviewList posts={pages} />
</DefaultPageLayout>

But how does Astro understand what's static and what's dynamic?

You can return params, paths and their content for sure, but how'd Astro turn the information passed from getStaticPaths into fully HTMLified files, available at the paths like /tags/x, /tags/y, etc.

The key is a special notation [param-key].astro. To use getStaticPaths the above code needs to be in speciall recoginzed file /src/pages/tags/[tag].astro or /src/pages/tags/[tag]/index.astro. We'll choose the later one. The value within [] will be replaced by the value you provide under params -> { tag: tag } to generate the files programmatically.

Commit the changes and see both the /tags, as well as the programmatically created tag pages come to life and work as expected.

Pagination

If you look at the tag page, you could easily see that the records are rendered all at once. It's okay if the number of posts under a tag may not go beyond a given limit, but realistically it happens seldom. In the scenarios of a multi-user blog, you cannot afford to pull all the data at once.

Here's where Astro's dynamic pages come to the rescue again. Dynamic pages let you break up a larger set of data into multiple, rightly-sized pages ex. 10 entries per page too.

The rules for marking a page dynamic and use getStaticPaths paths will still apply, but now you can use a paginate function to page the results as

paginate(filteredPosts, {
    params: { tag },
    pageSize: PAGE_SIZE
});

where Astro expects a return in getStaticPaths

Create a src/pages/tags/[tag]/[page].astro file and place the following code

src/pages/tags/[tag]/[page].astro
---
import { PAGE_SIZE } from '$/config'
import DefaultPageLayout from '$/layouts/default.astro'
import PostPreviewList from '$/components/PostPreviewList.astro'
import Paginator from '$/components/Paginator.astro'

let title = 'Posts By Tags'
let description = 'All the articles posted so far...'

export async function getStaticPaths({ paginate }) {
  const allPosts = Astro.fetchContent('../../blog/*.md')
  const allTags = new Set()
  allPosts.map(post => {
      post.tags && post.tags.map(tag => allTags.add(tag.toLowerCase()))
  })

  return Array.from(allTags).map((tag) => {
    const filteredPosts = allPosts.filter((post) => post.tags.includes(tag))
    return paginate(filteredPosts, {
      params: { tag },
      pageSize: PAGE_SIZE
    });
  });
}

const { page } = Astro.props
const { params } = Astro.request
---
<DefaultPageLayout content={{ title: `Posts by Tag: ${params.tag}`, description: `all of the articles we've posted and linked so far under the tag: ${params.tag}` }}>
    <PostPreviewList posts={page.data} />
    <Paginator page={page} />
</DefaultPageLayout>

This will result in pages like /tags/tag-x/1, /tags/tag-x/2, till there's even one single page left to fill in the PAGE_SIZE.

When you /paginate you just don't get filtered posts on data(as depicted above), you also get few additional keys to help in rendering page navigation controls.

The Paginator component we have imported has the following code

Paginator.astro
---
const { page } = Astro.props
---
<div class="page__actions">
    {page.url.prev && <a class="action__go-to-x" href={page.url.prev} title="Go to Previous">&larr; Prev</a>}
    {page.url.next && <a class="action__go-to-x" href={page.url.next} title="Go to Next">Next &rarr;</a>}
</div>
<style>
    .page__actions {
        @apply flex justify-center md:justify-end py-6 gap-2
    }
    .action__go-to-x {
        @apply text-base uppercase text-gray-500 dark:text-gray-400 hover:underline
    }
</style>

where the url property contains the metadata about the pages.

Trivia: Astro's Dynamic Page API has got perfected over the last few versions, and initially got introduced as the Collection API

Series Conclusion

Throughout this series, you got to meet Astro - the new JAMStack/SSG solution you must give a try, and learned about

How to set Astro up and get it running with essential defaults for styling, prototyping, and incremental deployments
Managing presentational components and creating pages
Feeding local as well as remote data to the pages
Putting interactivity where you absolutely cannot do without it, and finally about...
Creating data-driven, dynamic pages and paginated results

You can find the finished project here and live at Vercel. The idea was to cover the fundamentals and teach you about enough Astro to be dangerous!.

But, When you build a site with Astro(or any SSG solution), more concerns could come into play, which does not apply to just Astro but to the wider SSG ecosystem. Things like

  • How to make Astro(SSG) searchable?
  • How to support comments on a static site?
  • How can we feed data from a Headless or a JAMStack CMS into Astro etc. could play a significant role in your decision to choose Astro. But all of these will build upon the foundations we've discovered in this series. I plan to release standalone articles in the future covering such topics. If you want to explore the solutions available to serve such use cases, you should check awesome-jamstack out and look up to what people are doing in the SSG spaces for Hugo, Jekyll, 11ty, etc.

Additional Resources

Tweet, Discuss and DM me to show your appreciation. Consider sharing what you build and tag me at @aftabbuddy.

Thank You!

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