umma.dev

Astro

Here I go through how to set up a blog using Astro and how to integrate SSR into an Astro application.

What is Astro?

Usually used for static websites. It includes SSR support, so you can add different frameworks into it such as React. This is one the biggest advantages of using Astro. There might be a few pages on a website that need to be static and other pages where you have an SSR need such as fetching a data end-point.

Set Up

mkdir [dir-name]
npm init astro

Go through the options in the CLI, selecting the options which best suit the needs of the application you are building.

astro dev

Project Structure

Depending on selections made when setting up the project, will depend on the structure.

If you select the recommended you should have the follow:

node_modules
public
  favicon.svg
src
  components
    Card.astro
  layouts
    Layout.astro
  pages
    index.astro
  env.d.ts
.gitignore
astro.config.mjs
package.json
package-lock.json
README.md
tsconfig.json

Content Collections

This is the easiest way to work with Markdown within Astro and helps you oragnise content, validate frontmatter and provide automatic Typescript type-safety.

A content collection is any directory inside the reserved src/content.

// content.config.ts
import { defineCollection } from "astro:content";

export const collections = {
  blog: defineCollection({
    schema: z.object({
      date: z.date()
      categories: z.array(z.string())
      title: z.string()
    })
  }),
};

Create a folder within content called blog and add a simple markdown file to test the above.

blog/first-post.md

---
date: 01-01-01
categories: [test]
title: Hello!
---

# Hello World

Zod deals with the shape of the data and schema validations, it’s a run-time check. It will throw errors if the types given isn’t correct.

Pages and Components

Like many other frameworks, pages handle routing, data loading and overall page layout of every page.

These are the supported file types:

  • .astro
  • .md / .mdx
  • .html
  • .js/.ts (as end-points)
// index.astro
---
import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';

const blogs = await getCollection('')
---
<Layout title="Welcome to Astro.">
  <ul role="list" class="link-card-grid">
    {blogs.map((blog) => {
      return(
        <Card
          href={`/blog/${blog.slug}`}
          title={blog.data.title}
          body={blog.data.summary}
        />
      )
    })}
  </ul>
</Layout>

The page of blog post doesn’t exist at the moment. Create a blog directory within blog and then create a file called [slug].astro.

// [slug].astro

---
import { getEntryBySlug } from 'astro:content'
import Layout from '../../layouts/Layout.astro'

export async function getStaticPaths() {
  const allPosts = await getCollection('blog');
  return allPosts.map((post) => {
    return {
      params: { slug: post.slug },
      props: { post },
    }
  })
}

const { post } = Astro.props as { post: CollectionEntry<'blog'> };
const { Conent } = await post.render();
---

<Layout title={post.data.title}>
  <h1>{post.data.title}</h1>
  <p>Category: {post.data.categories.join(', ')}</p>
  <Content />
</Layout>

Routing

Each file within src/pages/ becomes an endpoint on your site, based on the file path. A single page can generate multiple pages via dynamic routing.

If you want to link between pages you can use the <a> tag.

End-points and Data Fetching

End-points export a GET which receives a content object with properties similar to Astro global.

Here is an example of using params and dynamic routing.

// src/pages/[id].json.ts
import type { APIRoute } form 'astro';

const usernames = ["one", "two", "three"];

export const get: APIroute = ({ params, request }) => {
  const id = params.id;
  return {
    body: JSON.stringify({
      name: usernames[id]
    })
  }
}

export functon getStaticPaths() {
  return [
    { params: { id: "0" } },
    { params: { id: "1" } },
    { params: { id: "2" } },
  ]
}

All end-points recieve a request property but in static mode you only have access to request.url.

// src/pages/request-path.json.ts

export const get: APIRoute = ({ params, request }) => {
  return {
    body: JSON.stringify({
      path: new URL(request.url).pathname,
    }),
  };
};
// src/components/User.astro

---
import Contact from '../components/Contact.jsx';
import Location form '../components/Location.astro';

const response = await fetch('https://randomuser.me/api/');
const data = await response.json();
const randomUser = data.results[0];
---

<!-- Data fetched at build can be rendered in HTML -->
<h1>User</h1>
<h2>{randomUser.name.first} {randomUser.name.last}</h2>

<!-- Data fetched at build can be passed to components as props-->
<Contact client:load email={randomUser.email} />
<Location city={randomUser.location.city} />

Integrating with Framework

Astro allows you to intergrate many different SSR frameworks. The example below is for React however you can mix components from different frameworks.

npx astro add react
npm install @astro/react

If you get the error cannot find package ‘react’ (or similar), do the following:

npm install react react-dom

Apply the integration to the astro.config.* file:

// astro.config.mjs

import { defineConfig } from 'astro/config';
import react from '@astrojs/react'

export default defineConfig({
  // ...
  integrations: [react()]
})
// src/pages/static-components.astro

---
import MyComponent from '../components/MyComponent.jsx';
---
<html>
  <body>
    <h1>Using React with Astro</h1>
    <MyComponent />
  </body>
</html>

Hydrating Interactive Components

A framework component can be made interactive (hydrated) using a client:* directive.

// src/pages/interactive-components.astro

---
import InteractiveButton from '../components/InteractiveButton.jsx';
import InteractiveCounter from '../components/InteractiveCounter.jsx';
import InteractiveModal from '../components/InteractiveModal.svelte';
---

<!-- This component's js will begin importing when the page loads -->
<InteractiveButton client:load />

<!-- This component's js will not be sent to the client until the user scrolls down the component is visible on the page-->
<InteractiveCounter client:visible />

<!-- This component won't render on the server but will render on the client when the page loads -->
<InteractiveModal client:only="svelte">