16 November 2022
by
Hector Sosa

Shipping Zero JavaScript with Astro

astro
javascript
Shipping Zero JavaScript with Astro
Astro is an all-in-one web framework designed for building content-focused websites. With this approach in mind, it delivers an incredible developer experience to build server-first, intuitive to use, flexible and fully-featured websites. Anyone who knows a little bit of HTML and CSS all the way up to seasoned developers can leverage Astro's opt-in complexity framework to build for the web.

Astro's biggest perks

Let's see what are the basics that Astro brings to the table, Astro is...
  • ...easy to use and by design less complex than any other UI framework or language (any valid HTML is a valid Astro component, see Why Astro - Easy to Use),
  • ...fast by default shipping ZERO Javascript out of the box, it takes an MPA approach loading on average 90% less JavaScript (see MPAs vs SPAs),
  • ...incredibly featured, flexible and UI-agnostic supporting several frameworks (supporting React, Preact, Svelte, Vue and more), an easier JSX, scoped CSS, file-based routing, data-fetching, and a lot more (see Integrations).
We've built a sample project called Astro for Docs (see source code: GH: Astro for Docs) to showcase how straight-forward and flexible it is to use Astro for Beginners.

What's in the menu for today?

After playing around with this project, it turns out you CAN REALLY build a content-focused website with ZERO JavaScript using Astro. Based on what we've learned, here's what we'd like to show you:
  1. Writing in "Astro" as a language to write Astro Components
  2. Use Markdown files as pages with Astro's MDX and TailwindCSS integrations.
  3. Design nested layouts for better developer experience.
  4. Leverage Astro's Runtime APIs to build components.
We ran npm create astro@latest with TypeScript and a blank, empty project.

Astro as a language

Will I have to learn a new programming language?

TLDR; no. If you know HTML, you already know enough to write an Astro component. At its core, its syntax is a superset of HTML. Capable of anything any other UI framework component can do, they are designed to feel familiar to anyone with HTML and JSX experience without any unnecessary complexity.
All Astro components are incredibly lean and have access to fetch remote data at build time, taking advantage of top-level await inside their component's script to render data in its component's template.
1/*** @file MyComponent.astro */
2---
3// Component Script (JavaScript)
4---
5<!-- Component Template (HTML + JSX Expressions) -->

Astro components are dynamic but NOT reactive

Unless you use Astro in SSG mode, Astro components are templates that only run once, at build time. Therefore, their values are dynamic (NOT reactive) and will never change. Let's check out some of the great features Astro brings to the table as opposed to conventional components:
1---
2// Template with multiple elements
3const DynamicTag = 'div'; // Dynamic Tags
4const items = ['2020', '2021', /*...*/]; // Fragments
5const visible = true; // Conditionally display
6---
7<DynamicTag>Hello!</DynamicTag> // renders as <div>Hello!</div>
8
9<p>
10  Astro supports multiple root elements in a template. Bye `<>`!
11  However, when using an expression to dynamically create
12  multiple children there is no need to declare a `key` but
13  you should wrap these inside a fragment as usual.
14</p>
15
16<ul>
17  {items.map(item => (
18    <>
19      <li>Halloween {item}</li>
20      <li>Thanksgiving {item}</li>
21    </>
22  ))}
23</ul>
24
25<p>Astro supports either `<Fragment/>` or its shorthand `<></>`.</p>
26
27{visible && <p>I'm alive!</p>} // This WILL show
However, writing content in HTML can be a monumental task out of itself. Which brings us to the question: How does Astro integrate local or remote content using Markdown or MDX?

Integrations Setup

What to know about integrations?

Astro Integrations can change the function and look of your projects with a single line of code. Astro has several official integrations to choose from. Broadly speaking, integrations can help you:
  • ...bring your UI framework of choice (React, Svelte, Vue, etc.),
  • ...change Astro into SSR mode, fetching all data at request time,
  • ...transform and model content easier (with MDX and TailwindCSS among others).
Getting started with integrations is incredibly easy. For official integrations, the astro add CLI automates installations for you. Installing with NPM you run npx astro add mdx and npx astro add tailwind to get your project bootstrapped with both. Detailed instructions on how to install and/or troubleshoot can be found in: Using Integrations.

What happens under the hood?

The astro add CLI, installs the NPM packages and applies the integrations into your astro.config.* file. Additionally, for TailwindCSS it creates its own tailwind.config.cjs file with the necessary file extensions for HTML, MD, MDX and all supported UI frameworks.

Nice combo to have

When working with Markdown files (locally or remotely) and TailwindCSS, we'd also recommend installing the following dev dependencies:
Banner

Do you need a reliable partner in tech for your next project?

Using MDX files

How to get started?

Once the integration is installed, it is as easy as adding .mdx files within your src/pages/ directory to get started. Let's swap our project's index.astro for an index.mdx to test this out. Regardless if you're building with Astro or not, please note these two files are not one-to-one. A standalone *.mdx file does require an HTML boilerplate to give our browser the full information about our project. So let's see how Astro helps us with this.
We can interact with our *.mdx files from the perspective of two parts: (1) Frontmatter, a composable YAML-based section to declare your document's meta data and (2) the body composed of MDX-syntax content.
1---
2layout: ../layouts/Page.astro
3title: Index
4# <- These key-value pairs will be accessible via
5# <- the `frontmatter` property from any `.astro` file
6---
7
8# Hello world
9
10<!-- This content will be accessible via -->
11<!-- the `<slot /> element -->
Anything written in inside the frontmatter fence (---) is accessible via the frontmatter property. So you can expect title in addition to the Astro provided key-value pairs to result in a payload like this (except for layout):
1{
2  title: 'Index',
3  file: '/../../../../../src/pages/index.mdx',
4  url: '',
5  astro: {}
6}

Where does TailwindCSS come in?

Let's get back to that layout property we declared in our frontmatter. Think of Layouts as reusable page shells where you'd inject content using the <slot /> element. They are useful for all of your HTML boilerplate and common UI elements (such as headers, navbars, footers and more). Layout components are commonly placed in a src/layouts directory in your project. So let's add a Page.astro layout file to see how that looks:
1---
2<!-- Server-side scripting can be done here -->
3const { frontmatter: title } = Astro.props;
4---
5<html lang="en">
6  <head>
7    <title>{title}</title>
8  </head>
9  <body>
10    <article>
11      <h1>{title}</h1>
12      <slot /> <!-- Your content is injected here -->
13    </article>
14  </body>
15</html>
Here is where you have HTML markup available to style your Astro project using TailwindCSS. In Astro, you are able to use the standard kebab-case format for all HTML attributes instead of the camelCase used in JSX. It even works for class, which is not supported by React.

Using TailwindCSS

All your styles in a single line

The official Tailwind CSS Typography plugin provides a set of prose classes you can use to add style defaults to any vanilla HTML you don’t control, like HTML rendered from Markdown, or pulled from a CMS with a single line of code. If we'd go back and add a couple of classes to our src/layouts/Page.astro layout file it would be enough to apply these styling defaults.
1---
2const { frontmatter: title } = Astro.props;
3---
4<html lang="en">
5  <head>
6    <title>{title}</title>
7  </head>
8  <body>
9    <main class="min-h-screen max-w-6xl mx-auto">
10      <article class="prose lg:prose-xl">
11        <h1>{title}</h1>
12          <slot />
13        </div>
14      </article>
15    </main>
16  </body>
17</html>

Styling individual HTML elements

There are plenty of element modifiers that you can use to add utility classes to style individual elements. Additionally, we'd recommend adding: dark:prose-invert, which triggers hand-designed dark mode versions for every default color theme.

How to use CSS in Astro

Styling an Astro component is as easy as adding a <style> tag in any *.astro file, where they are automatically scoped by default. You can also opt-out of scoping by adding the is:global attribute. If you aren't using TailwindCSS, advance styling languages like Sass and Less are also supported.
Say we want to change the look of an HTML element, even if we aren't authoring traditional CSS for our <a> and <pre> elements, could do something like this :
1<style>
2    .prose * > a {
3        @apply no-underline border-b-2 pb-1 hover:pb-2 transition-all;
4    }
5    .prose pre {
6        @apply p-5 drop-shadow-2xl;
7    }
8</style>
Don't abuse @apply just for the sake of making things look "cleaner". As Tailwind recommends: If you’re going to use @apply, use it for very small, highly reusable things like buttons and form controls — and even then only if you’re not using a framework like React where a component would be a better choice.

Nested Layouts

Let's talk basics

Layout components are conventionally used to provide a page shell for a given <slot /> of content. Incredibly useful to manage components, <html>, <head>, and <body> tags and other layouts too. However, Layouts don't need to contain an entire page of HTML. We are able to break down layouts into smaller layouts and then reuse them to create even more flexible pages.
Let's see how could we extract some boilerplate for our src/layouts/Page.astro layout by refactoring our code:
1---
2import Base from './Base.astro'
3const {frontmatter: title} = Astro.props;
4---
5<Base title={title}>
6  <article class="prose lg:prose-xl">
7    <h1>{title}</h1>
8      <slot />
9    </div>
10  </article>
11</Base>

Understanding nested layouts

So what's happening here? We are extracting all the HTML boilerplate <html>, <head>, <body>, and <main> tags to a Base layout. This new layout can take any props as needed. Here, we are passing the page's title for it to be rendered in our HTML's <head> tag.
Our Base layout will function the same way as our Page layout did before, now with the added benefit that it can be reutilized for other none-MDX pages. Here's how our Base layout looks like:
1---
2type Props = {
3    title: string;
4};
5
6const { title } = Astro.props;
7---
8<html lang="en">
9  <head>
10    <title>{title}</title>
11  </head>
12  <body>
13    <main class="min-h-screen max-w-6xl mx-auto">
14      <slot />
15    </main>
16  </body>
17</html>

Component Props

Props in layouts work as they do in any Astro component. On the example above, notice we are defining our props with TypeScript with the Props type interface. This is incredibly helpful to destructure our props from Astro.props and ensure typesafety throughout our project.

Typesafety in Layouts

Why Typesafety?

Typesafety is the extent to which a programming language prevents type errors. Naturally, when structuring an application with different layouts, components and pages, at the most fundamental level, it is useful to ensure that we are consuming the right types at all times.
Astro ships with built-in support for TypesScript out of the box. Within any of our *.astro files, we can author TypeScript code to prevent errors at runtime by defining the shapes of objects and components in our code. Let's talk about a couple of the recommended practices when using TypeScript for your Astro project.

Type Imports

In order to avoid duplication and maintain structure at scale, we recommend extracting all of your types into a separate *.ts file. Whenever importing any of these types, Astro recommends using explicit type imports and exports whenever possible. Use import type { SomeType } from './types instead of a traditional import. This avoids any edge-cases where Astro's bundler may incorrectly treat your imported types as if they were JavaScript.

Typesafety by example

We can add typesafety to our /*.astro files for our frontmatter's object by adding a category property in addition to our title and declaring its type by using the MDXLayoutProps helper:
1---
2import Base from './Base.astro'
3import type { MDXLayoutProps } from "astro";
4
5type Props = MDXLayoutProps<{
6    // Define any frontmatter property here
7    title: string;
8    category: string;
9}>;
10
11const {
12    frontmatter: { title, category },
13} = Astro.props;
14// Now your properties are typesafe in your script/template!
15---
16<Base title={title} category={category}>
17  <article class="prose lg:prose-xl">
18    <h1>{title}</h1>
19      <slot />
20    </div>
21  </article>
22</Base>

Astro's Runtime APIs

Working with Astro's toolbox

Astro's Runtime APIs is what really brings all of its power together. For example, we have been using Astro.props all along to read all the attributes passed as component props. You can browse their documentation to learn more about their APIs, see API Reference.
For now, let's introduce more of their APIs to build a Table of contents and a Side navigatation component.

Building a Table of contents

Astro.props is an object containing any values that have been passed as attributes. In addition to its frontmatter object, a Markdown layout will have access to the default props, such as url and headings among others (see Markdown Layout Props).
Excluding the frontmatter object, which we have already talked about, let's log the entire Astro.props to understand how easy Astro makes this information available to use:
1{
2  file: '.../src/pages/docs/runtime-apis.mdx',
3  url: '/docs/runtime-apis',
4  content: {
5    title: "Astro's Runtime APIs",
6    category: 'Runtime APIs',
7    file: '.../src/pages/docs/runtime-apis.mdx',
8    url: '/docs/runtime-apis',
9    astro: {}
10  },
11  frontmatter: {...},
12  headings: [
13    {
14      depth: 1,
15      slug: 'working-with-astros-toolbox',
16      text: 'Working with Astro’s toolbox'
17    },
18  ],
19  'server:root': true
20}
We can destructure the properties we need to access their values and work with them in a separate Astro component like this:
1/** LAYOUT: /src/layouts/Page.astro */
2---
3import TOC from "../component/TOC.astro";
4const { headings } = Astro.props;
5---
6
7<TOC headings={headings} />
8
9/** COMPONENT: /src/components/TOC.astro */
10---
11import type { HeadingsType } from "../lib/types";
12type Props = HeadingsType;
13
14const { headings } = Astro.props;
15---
16
17<ul>
18    {headings.map(({ slug, text }) => (
19        <li><a href={`#${slug}`}>{text}</a></li>
20    ))}
21</ul>
22

Building a Side Navigation

For a side navigation, we do need to read all the files within a given folder in our project. In order to do that, Astro also offers another API for this purpose. Astro.glob() is the easiest way to load multiple files into your static site setup. It only takes a single parameter: a relative URL glob of which files you'd like to import.
Each of our /*.mdx pages has a title and a category. We'd like to group each page by category and display them in a component for navigation. Let's take a look how we build a Side navigation component using Astro.blog():
1import type { MDXInstance } from "astro";
2import type { DocsType } from "../lib/types";
3
4const pages = (await Astro.glob("../pages/docs/*.mdx")) as Array<
5    MDXInstance<DocsType>
6>;
7
8const sortedPages = pages.sort((a, b) =>
9    a.frontmatter.category.localeCompare(b.frontmatter.category, "en", {
10        sensitivity: "base",
11    })
12);
13
14const categories = [
15    ...new Set(pages.map((p) => p.frontmatter.category)),
16];
Once we have this information gathered, we can dynamically create a component mapping over our categories and creating children if they belong within that given category.
A huge thing to note is that Astro.glob() is a wrapper of Vite's import.meta.glob() API, and it's only available in /*.astro files. In case you need access to your files in other file extension types, you can always use Vite's API (see Vite's Glob Import).
That's all there is to it, if you'd like to see the source code in more detail, feel free to check out: GH: Astro for Docs.

Conclusion

  • Astro does deliver a great DX regardless of your level of experience, as building Astro components only really requires HTML and CSS.
  • Astro is UI-agnostic, meaning you can BYOF it and bring your own framework. They have official support for React, Preact, Solid, Svelte, Vue, and Lit. You can even mix and match different frameworks.
  • Astro shines with statically generated content. Even if there is no SSR or ISR like Next.js, Astro is considering per-page support for SSR in their 2022 Q4 roadmap.
  • Astro can ship ZERO JS (you'd be surprised how many things can be achieved with only HTML and CSS). Regardless if you need to introduce Astro Islands and UI frameworks, it loads 40% faster with 90% less JavaScript in comparison to modern frameworks.
Feel free to explore this example using the resources below:
Share post

Let’s stay connected

Do you want the latest and greatest from our blog straight to your inbox? Chuck us your email address and get informed.
You can unsubscribe any time. For more details, review our privacy policy