Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: Astro Foundations

In this Code Lab, you'll build GloboBlog (GloboTicket's new blog, where they share news, conference highlights and other information with their customers) using Astro to achieve fast performance, maintainable content, and modern interactive features. You'll start from scratch by creating a new Astro project, add static pages and markdown blog posts, then sprinkle in interactive components using Astro's islands architecture, and you'll see how Astro can be combined with React and Vue. Finally, you'll optimize performance so the blog loads fast for event-hungry users.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 1h 10m
Published
Clock icon Feb 11, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Introduction

    In this Code Lab, you'll build GloboBlog, a content-driven blog for GloboTicket, a global ticket provider. GloboBlog will feature event highlights, news, and other information for tech enthusiasts. You'll use Astro, a modern static site generator, to create a fast, interactive, and maintainable blog. By the end of this lab, you'll have a fully functional blog site with static and dynamic content, interactive components, and optimal performance. ## Why Astro?

    Astro is a modern static site generator that combines the best of static and dynamic web development. It allows you to build fast, content-driven websites with dynamic client-side interactions. Astro's unique architecture enables you to combine static content with interactive components that hydrate on demand, providing a seamless user experience without sacrificing performance or SEO (Search Engine Optimization). With Astro, you can build modern websites that are fast, accessible, and maintainable.

    Some features include:

    • File-based routing: Astro automatically generates routes based on the file structure of your project, allowing static and dynamic routes.
    • Markdown support: Astro allows you to create content-driven pages using markdown files, and automatically renders them as HTML, improving the editing experience for content authors.
    • Content collections: Astro allows you to manage content collections for dynamic data, such as blog posts or product listings, allowing you to define schemas for the content, validate it at build time, and generate dynamic routes for each item in the collection.
    • Components and Partials: Astro allows you to create reusable components and partials, allowing you to create a consistent design across your site, and improve the maintainability of your code.
    • Supports Popular Frameworks: Astro allows you to define Astro components, but also supports popular frontend frameworks like React, Vue, and Svelte, allowing you to integrate different types of dynamic components seamlessly, and benefit from the ecosystem of each framework.
    • Islands architecture: Astro's islands architecture enables you to create interactive components that hydrate on demand, reducing the initial load time of your site, and combining the best of server-side rendering and client-side interactivity (static and dynamic content in the same page). This is ideal for performance and SEO.
    • Performance optimization: Astro provides tools and best practices for optimizing site performance, such as image optimization with the Image component, partial hydration with Islands, and removing unused JavaScript from server-side rendered pages.
    • Lightweight and Simple: Overall, Astro is lightweight and easy to use, making it ideal for building fast, content-driven websites.
  2. Challenge

    Step 1a: Setting Up the Astro Project

    Step 1a: Setting Up the Astro Project

    In this step, you'll initialize an Astro project and explore the default project structure. You'll learn how Astro handles file-based routing and how to create static pages using Astro's file structure. ## Initialize the Astro Project

    In this lab environment, the Astro project is already set up for you. However, if you were setting up a new Astro project locally, you would run the following command to create a new Astro project:

    npm create astro@latest -- --template minimal
    

    This would set up a minimal Astro project with the necessary files and configurations. For this lab, you can open the project in your editor and explore the files that have been provisioned for you. The project structure includes:

    • A package.json file with the necessary dependencies, in this case astro is the main dependency. @astrojs/node and @astrojs/react are required to use the Node and React integrations, as you will see later in the lab. The remaining dependencies and devDependencies are required for testing the project.
    • An astro.config.mjs file for Astro configuration. Here you can add extra integrations, as you'll see later in the lab. For now, this includes the vite.server.allowedHosts: true and server.host: true config options to ensure the server can be accessed from the browser tab, and the integrations section with the react() integration. The adapter.node integration is commented out, as you will turn it on and off later in the lab.
    • A src folder for your page's content. This include some folders which are part of Astro's default project structure, as well as some folders that are just for organizational purposes:
      • The pages subfolder, which Astro will use for file-based routing, where each file represents a route in your application. Astro supports .astro, .md, .mdx (with the @astrojs/mdx integration) and .html files as routes, and index.<extension> files are used as the default / route. For now, this just includes the index.astro file.
      • The content subfolder, which Astro will use for content collections, as you'll see later in the lab. For now this files are ignored by the project, although you might see some warnings in the console. These will go away once you correctly set up the content collections later in the lab.
      • The components subfolder, is where you will store your reusable components, although Astro does not require you to do this.
      • The layouts subfolder, is where you will store your reusable layouts, although Astro does not require you to do this.
      • The styles subfolder, already contains a global CSS file to avoid writing styles as part of the lab. Again, Astro does not impose a required structure for styles.
      • The images subfolder, is where you will store your images, which are used in the blog posts. Placing images somewhere inside src instead of the public folder will allow Astro to optimize them and serve them as static assets when you use the Image component, as you will see later in the lab.
    • A public folder for static assets like images, fonts, and other resources. ### Running the Astro Project

    To run the Astro project in this lab environment, you can use the following command:

    npm run dev
    

    This command starts the Astro development server and serves your project locally, reflecting any changes you make in real-time.

    Now, open the project in your browser at {{localhost:4321}} to see the default home page of your GloboBlog site. You should see a simple "Astro" h1 title on the home page. ## File-based Routing and Astro files

    In Astro, file-based routing is a powerful feature that allows you to create routes based on the file structure of your project. Each file in the src/pages directory represents a route in your application. For example, the src/pages/index.astro file corresponds to the root / route of your application.

    Astro files have the .astro extension and can contain HTML, CSS, and JavaScript. Astro files allow you to define components, layouts, partials, and pages in a single file, making it easy to create and manage your project. Astro files support JSX/TSX syntax and can include dynamic content using Astro's built-in features.

    The basic structure of an Astro file includes script and template sections, separated by a triple dash ---. The script section contains JavaScript or Typescript code, while the template section contains HTML and JS expressions.

    ---
    // Component Script (JavaScript or TypeScript)
    ---
    <!-- Component Template (HTML + JS Expressions) -->
    
  3. Challenge

    Step 1b: Using Components

    Step 1b: Using Components

    Components

    Astro allows you to create reusable components to define a piece of UI that can be reused multiple times without repeating code. Besides reusability, components allow you to encapsulate logic and state, making the codebase more maintainable and easier to reason about, and they can be tested in isolation from each other. With Astro, you can not only use Astro components, but also use frameworks like React, Vue, and Svelte.

    Like a page, an Astro component is a file with the .astro extension that contains a script and template section. The script section contains the logic and state of the component, while the template section contains the HTML and JS expressions.

    Components can encapsulate logic and state, which are defined in the script section. You can also pass props to components, so that different instances of the same component can be rendered with different values. These props are defined in the script section, and can be accessed in the template section using the Astro.props object.

    Astro components work in a similar way to React components, in that they can use JSX/TSX syntax, which is syntactical sugar for the JavaScript/TypeScript language to make it more declarative and similar to HTML. This includes adding JS expressions within {} inside the HTML in the template section.

    Components can also include styles, which are scoped to the component by default. This means that the styles are not applied to the rest of the page, and are only applied to the component itself.

    Example: Navbar Component

    For example, take a look at the Navbar.astro component in the src/components directory. This component includes a nav element with links to other pages:

    ---
    type Props = {
      links: { href: string; text: string }[];
    };
    
    const { links }: Props = Astro.props;
    ---
    
    <nav class="container">
      <ul>
        {links.map((link) => (
            <li><a href={link.href}>{link.text}</a></li>
        ))}
      </ul>
    </nav>
    
    

    Where links is an array of objects with href and text properties, and the map function is used to iterate over the array and render a list item li with an anchor a element for each link.

    To use this component in another page or a parent component, you could include it as follows:

    ---
    import Navbar from './components/Navbar.astro';
    import type { ComponentProps } from 'astro/types';
    
    const links: ComponentProps<typeof Navbar>['links']  = [
      { href: '/', text: 'Home' },
      { href: '/about', text: 'About' },
      { href: '/blog', text: 'Blog' },
    ];
    ---
    
    //...
    
    <Navbar links={links} />
    

    Where links is an array of objects with href and text properties, and the Navbar component is imported from the ./components/Navbar.astro file. Note that the import type { ComponentProps } from 'astro/types'; is used to import the ComponentProps type from the astro/types module, which is used to define the type of the links prop by reusing the links type from the Navbar component, even though it's not explicitly exported from the component.

    Styles in Astro Components

    Astro allows you to define styles in your components, which are scoped to the component by default. This means that the styles are not applied to the rest of the page, and are only applied to the component itself. To do this, you can use the <style> tag in the component.

    <style>
    /* Your CSS code here */
    </style>
    

    To apply global styles, you can use the is:global directive.

    <style is:global>
    // Global styles
    </style>
    

    But you can also import a CSS file in the component, and it will be applied to the component.

    import "../styles/base.css";
    

    Finally, you can use CSS modules to style your component.

    <style module>
    // CSS modules
    </style>
    

    or:

    import styles from "./styles.module.css";
    

    As this lab focuses on Astro features, global styles are already included in the project, as you can see in the src/styles/base.css file. You will use this file to apply global styles to a layout that will wrap all pages of your site.

  4. Challenge

    Step 2a: Creating Content-Driven Pages

    Step 2a: Creating Content-Driven Pages

    In this step, you'll create static content-driven pages for your GloboBlog site using markdown files. Static pages are ideal for content that doesn't change frequently, and the server will return the same HTML for each request, making it very fast and easier to cache and optimize. Markdown files provide a simple and structured way to create content, making it easy to manage and update, potentially even by non-technical users. ## Creating Markdown Files

    Markdown is a lightweight markup language that allows you to write content using plain text formatting. Markdown files have the .md extension and can be used to create blog posts, articles, and other content. Markdown files are easy to read and write, making them a popular choice for content creation.

    A markdown file is a simple text file that can include formatting using a few symbols, like # Title for headings, *bold* for bold, _italics_ for italics, and [text](url) for links. Additionally, markdown files can include frontmatter metadata, which is a way to include metadata about the file in the file itself. This metadata is used by Astro to generate the HTML page, and is included between --- lines at the top of the file. ## Content Collections and Dynamic Routes

    As you noticed, the blog posts are stored as markdown files in the src/pages/blog directory. So, you would create a new file for each blog post, and astro would automatically render each one as a static page in the /blog/<post-file-name> route. However, this is not ideal, as you have no control on the style, you can't use a uniform layout, or taking advantage of a uniform schema in the blog posts to render and validate them.

    To address this, you will first use a dynamic route to render the blog posts, and then you will refactor the code to use a Content Collection. Content Collections are a feature in Astro that helps you organize and validate your content. They provide a type-safe way to work with Markdown, MDX, and other content files in your project.

  5. Challenge

    Step 2b: Server Side Rendering, Client Side Rendering and Static Site Generation

    Step 2b: Server Side Rendering, Client Side Rendering and Static Site Generation

    In the previous task, you created a dynamic route for your blog posts. However, if you try to access a new blog post, for example {{localhost:4321}}/blog/second-post, you will notice an error: getStaticPaths() function is required for dynamic routes. Make sure that you export a getStaticPaths function from your dynamic route.

    To understand what is going on, you need to understand the difference between Server Side Rendering, Client Side Rendering and Static Site Generation.

    • Server Side Rendering (SSR): the server renders the page on the server side on each request, and returns static HTML to the client. The server does all the heavy lifting, and the client's browser receives the finished HTML and just displays it as it comes from the server.
    • Client Side Rendering (CSR): the server returns a minimal HTML page, and the client's browser uses JS to render the rest of the page. The server returns HTML and JS, and the client's browser does the heavy lifting to render the page, turning it into the final HTML that the client's browser displays. The process of rendering HTML in the client's browser using JS is called hydration.
    • Static Site Generation (SSG): similar to Server Side Rendering, but instead of generating an HTML page on each request, the server generates the HTML at build time, and then returns the static HTML to the client. This is useful for content that doesn't change frequently, so the server can pre-render the page at build time, and then return the static HTML to the client, improving performance and load times.

    Astro uses Static Site Generation by default, which means that the server generates the HTML at build time, and then returns the static HTML to the client. This is why you get an error when you try to access a new blog post, as the server has not generated the HTML at build time. You need to either specify which paths need to be generated at build time, or use Server Side Rendering so that the server generates the HTML at runtime. ### Get Static Paths

    If you want to keep using Astro's default Static Site Generation, you need to specify which paths need to be generated at build time. This is done by implementing the getStaticPaths function in the dynamic route. This function returns an array of paths for the dynamic route. Each path is an object with a params property that contains the dynamic route parameters. In this case, the slug parameter is used to extract the slug from the URL.

  6. Challenge

    Step 2c: Setting Up Content Collections

    Step 2c: Setting Up Content Collections

    So far, you used markdown files and dynamic routes (both SSR and SSG). However, this is not the only way to create content-driven pages in Astro. You can also use Content Collections, which are a feature in Astro that helps you organize and validate your content. They provide a type-safe way to work with Markdown, MDX, and other content files in your project.

    With Content Collections, you can define a schema for your content, and then use this schema to validate your content. You can also use this schema to render your content in your pages. Think of it as a combination of dynamic routes with SSG and markdown files, but with a type-safe way to work with your content.

    In this case, you will not directly use markdown files in the src/pages directory, so Astro will not automatically render them as static pages. Instead, you will place the markdown files in the src/content directory, define a Content Collection for them, which will define a schema that each blog post needs to follow. And finally, you will use a Dynamic Route with SSG to render the blog posts, defining a template for the page, as you know that each blog post will follow the schema you defined in the Content Collection.

    In this way, Astro combines the efficiency of dynamic routes with SSG, the simplicity of markdown files, and the type-safety of Content Collections, leveraging the frontmatter metadata of markdown files to define the schema for the content. ### Defining a Content Collection

    Astro uses a content.config.ts file to define the collections for your content. In this file, you need to import the defineCollection function from astro:content, and then invoke it passing an object with the following properties:

    • loader: indicates how to load the files for the collection content. Astro provides 2 built-in loaders: glob and file in the astro/loaders module. The glob loader is used when you have a pattern for the files you want to include in the collection (e.g. multiple markdown files in a directory), and the file loader is used when you have a single file that defines the whole collection (e.g. a single JSON file).
    • schema: (optional, but highly recommended) a Zod schema that defines the structure of the content in the collection. Zod is a schema declaration and validation library for JavaScript. In this case, you import it as z from astro:content. This library provides common types like z.string(), z.number(), z.boolean(), z.date(), etc. It also provides a z.object() type, which allows you to define an object with a set of properties, each with a specific type. And additionally, it provides options to further validate the content, for example z.string().min(1), which ensures that the string is at least 1 character long, or z.string().optional(), which allows the string to be optional.

    The defineCollection function returns a collection object. To be able to use it in your project, you will need to export a collections object from your content.config.ts file, with the collection name as the key, and the collection object as the value. In this case, you only define a single collection, so you will only have a single key named blog in the collections object.

    import { defineCollection, z } from "astro:content";
    import { glob } from "astro/loaders";
    
    const blogCollection = defineCollection({
      loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
      schema: z.object({
        title: z.string(),
        // In YAML, dates written without quotes around them are interpreted as Date objects
        publishDate: z.date(), // e.g. 2025-01-01
        imageUrl: z.string().optional(),
      }),
    });
    
    export const collections = {
      blog: blogCollection,
    };
    ``` ### Using the Collection
    
    Once you have defined the collection in the `src/content.config.ts` file, you can use it in your project by importing the `getCollection` function from the `astro:content` module, and then using it in a dynamic SSG route to get the collection entries and pass them to the `getStaticPaths` function.
    
    The `getCollection` function receives a string which is the name of the collection you want to get, corresponding to the key in the `collections` object that was exported in the `content.config.ts` file. It returns an array of collection entries. Each entry is an object with:
    
    - `id`: the id of the collection entry. This is a unique identifier for the collection entry, and is generated by Astro.
    - `data`: the data of the collection entry, which is defined in the frontmatter metadata of the markdown file, and follows the schema you defined in the `defineCollection` function.
    - `body`: the body of the collection entry, which is the content of the markdown file, without the frontmatter metadata.
    
    However, to actually use the collection entries in your project, you need to render them in your template. This is done by importing the `render` function from the `astro:content` module, and then using it to render the collection entry as HTML. The `render` function receives the collection entry and returns an object with the following properties:
    
    - `Content`: the rendered HTML of the collection entry. You can use this as a component in your template.
    - `headings`: an array of headings in the collection entry.
    
    ```astro
    ---
    import { getCollection, render, type CollectionEntry } from "astro:content";
    import type { GetStaticPaths } from "astro";
    import BaseLayout from "../../layouts/BaseLayout.astro";
    
    // 1. For getting the collection entries, you can use the getCollection function
    export const getStaticPaths: GetStaticPaths = async () => {
      const posts = await getCollection("blog");
      return posts.map((post: CollectionEntry<"blog">) => ({
        // Use the id of the collection entry as the slug that will be used in the dynamic route.
        // You need to remove the file extension from the id, as it is not needed in the slug.
        params: { slug: post.id.replace(".md", "") },
        props: { post },
      }));
    };
    
    // 2. For rendering in the page, you can get the entry directly from the prop
    const { post } = Astro.props as { post: CollectionEntry<"blog"> };
    
    // 3. You can then render the collection entry to HTML as a component
    const { Content } = await render(post);
    ---
    
    <BaseLayout title={post.data.title}>
      <!-- You can access the frontmatter metadata in the post.data object, knowing that the entry follows the schema you defined in the content.config.ts file  -->
      <h1>{post.data.title}</h1>
      <div class="post-content">
        <!-- You can render the collection entry to HTML as a component -->
        <Content />
      </div>
    </BaseLayout>
    
  7. Challenge

    Step 3: Implementing Islands Architecture

    Step 3: Implementing Islands Architecture

    In this step, you'll implement islands architecture in your GloboBlog site. Islands architecture is a modern approach to building interactive web applications that allows you to create interactive components that hydrate on demand, providing a seamless user experience without sacrificing performance.

    One of the limitations of SSR and SSG is that the HTML is rendered on the server side, and the client-side JavaScript is not executed. This means that the page is not interactive until the client-side JavaScript is loaded.

    Astro's islands architecture combines the benefits of static site generation (SSG) with the interactivity of client-side rendering (CSR), allowing you to create fast, interactive web applications.

    Basically, Astro will generate all the HTML it can on the server side, and then it will generate "islands" of interactivity that will be rendered on the client side, by executing JavaScript in the client's browser. ## How hydration works

    Basically, hydration is the process of converting static HTML into interactive components. Full CSR will send a minimal HTML with an empty body in the response, and the client's browser will load the JavaScript and hydrate the components. This has a few drawbacks:

    • Loading a page might include a long delay, as more files need to be downloaded and executed. Sometimes the client ends up making multiple requests to load a single page. In general, servers will have better performance to load all resources compared to the client, particularly for mobile devices or bad network connections.
    • SEO and accessibility might be negatively impacted, as the client's browser will not be able to render the page without JavaScript. This can cause the search engines to only see a very limited page, or cause layout shifts and other accessibility issues once the page is hydrated.

    With Astro's islands architecture, you can combine the benefits of SSR and SSG with the interactivity of CSR, by only hydrating the components that are needed for the user's interaction. In this way, most of the page is rendered on the server side, and only the components that are needed for the user's interaction are hydrated on the client side. ### Astro's Dev Help Bar

    When running in dev mode, Astro will show a help bar floating at the bottom of the page. If you hover the mouse cursor over it, you will see 4 buttons. The second one is the "Inspect" button, which will highlight the islands in the page.

    Astro's Dev Toolbar

    For now this will not show any islands, as our site uses only SSG.

    Integrating React

    Astro supports popular frontend frameworks like React, Vue, and Svelte, allowing you to integrate dynamic components seamlessly. For this lab, you will add a React component to your site.

    For integrating React, you would need to run npx astro add react to install the React integration. This has already been done for you in the project, so the package.json file already includes @types/react, @types/react-dom, react and react-dom. This also added the JSX configuration to the tsconfig.json file, and the integrations: [react()] to the astro.config.mjs file.

  8. Challenge

    Step 4: Optimizing Site Performance

    Step 4: Optimizing Site Performance

    Astro has a lot of built-in optimizations, including automatic removal of unused CSS and scripts, and automatic image optimization when using the Image component.

    The dev toolbar also comes with an "Audit" button, which will show you a report of the performance of your site. For example, it will warn you if you're using the img element instead of the Image component, or if you're not using the width and height attributes in the img element.

    You already saw how using islands allows you to combine the benefits of SSR and SSG with the interactivity of CSR, by only hydrating the components that are needed for the user's interaction.

    This section will only include a few basic tasks to show you how the Image component works, but feel free to explore the differences using Astro's dev toolbar, and your own browser's dev tools to inspect the performance of your site. A few things to try:

    • Compare the performance of the img element and the Image component. For the img you will need to copy the image to the public/images directory, and then use the src attribute to point to it. Also, check what happens if you don't use the width and height attributes in the img element, or if you provide different values for the decoding and loading attributes.
    • Compare the differences between SSR, SSG and CSR, and how using islands allows you to have the best of both worlds. ## Images in Astro

    Astro allows you to optimize images for performance, choosing the best format and size for the image to load the fastest possible, and forces you to specify the width and height of the image, to avoid layout shifts, which is a best practice for accessibility.

    To achieve this, and many other features and optimizations, Astro has a dedicated Image component, which is a wrapper around the img element.

    ---
    import { Image } from "astro:assets";
    ---
    
    <Image
      src="/images/test.jpg"
      {/* The alt text is required for accessibility */}
      alt="A beautiful bird"
      {/* The width and height are required to avoid layout shifts */}
      width={100}
      height={100}
      {/* The decoding attribute allows the browser to decode the image asynchronously, improving performance, as it allows the browser to start rendering the page while the image is loading. */}
      decoding="async"
      {/* The loading attribute allows the browser to load the image eagerly, as it is visible in the viewport on the page load. You could use "lazy" to defer the loading of the image until it is visible in the viewport. This is useful for performance, as it allows the browser to load other resources first. */}
      loading="eager"
    />
    
  9. Challenge

    Conclusion

    Conclusion

    Well done! You've completed the lab. You now know how to use Astro to build a static content-driven site, and you know the main features and concepts of Astro.

    There's much more to Astro, and you can explore more features and concepts like:

    • Using other integrations like Vue or Svelte.
    • Using other integrations like Tailwind CSS.
    • Using MDX to mix Markdown with JSX/TSX.
    • Using API routes to add server-side logic to your site.
    • View Transitions and prefetching.

    And much more!

Julian is a Backend Engineer working with Java, Python, and distributed cloud systems. He has worked in Amazon, Google, and various startups, and focuses on first principles over tools.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.