I recently started using Astro to rebuild side projects that were originally built with WordPress, Go, Rails, and Hugo. I picked Astro because it had a React-like DX with good language server support, and because it was compatible with serverless hosting platforms that had a generous free tier (Cloudflare, AWS Lambda, etc).
I didn't know much about Astro when I started using it. Now that I've migrated multiple sites, I wanted to share what I liked and disliked about the framework for anyone else considering using it.
Astro: what I liked
At its core, Astro is a static site generator with the ability to produce dynamic server-rendered pages when needed. Astro is a great fit for blogs or small marketing sites with limited interactivity. The framework provides most of the DX from Next.js without the React.js overhead.
Good IntelliSense and code formatting in a server rendered template language
Let's be honest: good language server support and code formatting are severely lacking in traditional server-rendered templating languages. Go templates, Jinja, ERB, and EJS are painfully behind with tooling when compared to their React/Vue/Svelte counterparts. Most server-rendered templating languages have no way of knowing what variables are in scope or what their types are.
With Astro, all of these features are one VS Code extension away.
Inside of Astro templates, you set your data at the top of the template inside of a "code fence" that either runs at build time or when responding to a request on the server. Here's what that looks like in practice:
---
import Layout from "../layouts/Layout.astro";
import { getPosts } from "../data/posts";
const posts: { id, title }[] = await getPosts();
---
<Layout pageTitle="Posts">
<h1>Posts</h1>
{post.length > 0 ? (
<ul>
{posts.map((post) => (
<li>
<a href={`/posts/${post.id}`}>
{post.title}
</a>
</li>
)}
</ul>
) : null}
</Layout>
Because all of the data for the template is loaded in the "code fence" above the template, the language server can provide auto-completion for the properties of any object defined within the scope. It will also indicate when you are trying to use a variable that doesn't exist.
Astro files can be "components"
One of my biggest gripes with traditional templating languages like Go templates, Jinja, and EJS is that they don't have "components" that can accept children. Most of my websites have a constrained-width "container" UI element of some kind, which ensures that content doesn't fly out to the end of the screen on ultra-wide monitors. If you have a .container
class that you manually add to <div />
elements, then this usually works fine. However, if you're using a utility CSS framework like Tailwind then you may find yourself adding the following code to every single page template:
<div
class="mx-auto px-4 sm:max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl"
>
<!-- Inner-page content goes here... -->
</div>
When you eventually need to change these classes, it's an error-prone pain to update each file manually. But if your templating language doesn't have components that can accept children, it's almost inevitable.
Unlike those traditional templating languages, Astro templates can be used as components that accept children using a <slot />
tag. A long string of utility classes could be extracted into a reusable Astro component:
<div
class="mx-auto px-4 sm:max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl"
>
<slot />
</div>
The Astro component could then be consumed from another Astro file.
---
import Container from "../components/Container.astro";
---
<Container>
<h1>Hello, world!</h1>
</Container>
Astro files aren't limited to a single slot: they can have multiple.
My favorite feature of Astro components is that they can accept props within the code fence. Here's an example:
---
type Props = { pageTitle: string; pageDescription: string };
const { pageTitle, pageDescription } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
</head>
<body>
<slot />
</body>
</html>
The component can then accept props when used within another file.
---
import Layout from "../layouts/Layout.astro";
---
<Layout
pageTitle="Tyler's Blog"
pageDescription="I don't really post on here."
>
<h1>Tyler's blog</h1>
<p>Sorry, there's no content yet...</p>
</Layout>
A frontend asset pipeline with live reloads is built-in
I've built my own server-side integrations with Vite before. If you're trying to get something online quickly, this is the kind of commodity feature that you want to avoid building yourself. With Astro, it's built-in.
If you want to add a custom script to an Astro page or component, all you have to do is drop a script tag on the page.
<div>
<h1>This is my page</h1>
<script src="../assets/script.ts"></script>
</div>
Astro will automatically compile TS and JS files as a part of a site's build process. You can also write scripts that use imports from node_modules
inside of a script tag within an Astro component, and Astro will compile that during build time and extract it to its own file.
<script>
// This will also compile down to a JavaScript file at build time.
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios";
const response = await axios
.get<AxiosRequestConfig, AxiosResponse<{foo: string}>>("/some-endpoint");
console.log(response.data.foo);
</script>
You can include CSS or Scss styles in an Astro file by importing them within the code fence.
---
import "../assets/styles.css";
---
Astro also provides the ability to do scoped styles by using a style tag in an Astro file. This feature may be familiar to Vue users.
Given the following Astro file:
<style>
.heading {
color: red;
}
</style>
<h1 class="heading">
Hello, world!
</h1>
Astro will render the following:
<style>
.heading[data-astro-cid-6d7mwtum] {
color: red;
}
</style>
<h1 class="heading" data-astro-cid-6d7mwtum>
Hello, world!
</h1>
The injected data-astro
attributes scope the styles to the current component, ensuring that they don't override styles in other components that may use the same class name.
First-class markdown & MDX support
I've lost days trying to figure out the different ways to incorporate markdown and MDX into Next.js. With Astro, it's built-in. Here's what it looks like in practice:
---
import { Content, frontMatter } from "../content/blog-post-1.md";
---
<h1>{frontMatter.title}</h1>
<Content />
Easy, right?
Actions
Actions provide a type-safe way of running backend functions. They provide validation and can handle both JSON and form data. They're easily one of Astro's killer features: all of this would need to be hand-wired in a Next.js app in a bespoke way. They require a little more code than can fit neatly into an example, but they're pretty elegant to use. I'd recommend reading the actions docs page.
No runtime JS cost
There are an endless number of Twitter devs that say React is "fast enough." For a lot of things it's not.
I use Rasbperry Pi 4s for little projects, and you can feel the runtime cost of React. I'm sure it's the same for inexpensive Android phones, except in that case the JS overhead will also drain the battery.
If the only interactivity that my site needs is toggling a nav menu, I'd rather wire that up myself. I'll happily reach for React when I need it, but for so many projects I don't actually need it.
Astro: what I disliked
The things that I dislike about Astro are not unique to the framework: they are ideas borrowed from other tools in the JavaScript ecosystem.
File-based routing
Because Astro employs file-based routing, half of the files in an Astro project end up named index.(astro|js|ts)
or [id].(astro|js|ts)
. File-based routing is an obnoxious pattern that swept the frontend world by storm after Next.js implemented it, and it comes with many drawbacks:
- You'll often have 5 tabs open in your editor with the same filename. It will take a minimum of 3 guesses to find the tab you're looking for.
- The editor's file fuzzy finder is far less useful because so many files have the same name.
- File-based routing scatters one of an application's core concerns across many files and folders, making it difficult to see all routes at a glance within the editor. Instead, you must drill down into several folders to understand what pages are available.
I'll admit: file-based routing feels great when you're making a site with under 10 pages. But as a site grows it adds friction, and you fight the feature more than you benefit from it.
In the JavaScript ecosystem, Remix stands apart by offering a version of file-based routing that flattens all routes into a single directory, and allows a user to opt out of file-based routing entirely with manual route configuration.
File-based routing is my biggest complaint about Astro, but it's a difficult feature to escape. It is implemented in Next.js, Nuxt.js, SvelteKit, and others. What's even stranger is that these frameworks are largely unopinionated about the filenames for other parts of the application. In start contrast to Ruby on Rails, most JavaScript frameworks give you a great degree of freedom in file names and project structure–except for routing. It's a special case, and special cases add complexity to software.
One component per file (kind of)
A JavaScript language feature that I really like is the ability to define multiple variables, functions, and classes in a single file. This makes it easy to colocate related functionality without having to prematurely extract it to other files because of language-level constraints.
Much like Vue's single-file components, Astro files allow defining one component per file. This feels tedious to me, but Astro provides a workaround.
Astro can embed pre-rendered React, Vue, Svelte, Solid, and Preact components directly into its templates without loading any client-side JavaScript. Preact components pair reasonably well with Astro because Preact JSX is much closer to HTML than React JSX. It does become awkward managing both Astro and Preact components in the same project though, and once I begin using Preact components I find myself moving most of the UI out of Astro files and into Preact.
Final thoughts on Astro
If you're an avid user of Next.js, Nuxt, or SvelteKit and you are happy with your framework, you might not get much out of Astro. However, if you want to ship less JavaScript bloat to your users while retaining the DX of something like Next.js, then Astro might be for you.
Astro is geared towards content-driven websites, and provides markdown support out-of-the-box. Because of its focus on content, it is an ideal developer blogging platform to replace a WordPress or Hugo site. However, it's capable of much more than just content sites through features like Actions.
Despite my strong distaste for file-based routing, my biggest concern with adopting Astro is the potential for breaking changes that would force me to rebuild my sites. JavaScript tools are much more aggressive with breaking changes than tools you find in other language ecosystems. Because I'm so new to Astro, I don't know how much changes from one major version to the next. Even with this concern, I plan to move 5-to-6 of my sites from other platforms to Astro so I can take advantage of its top-notch DX and host the sites inexpensively.
Top comments (6)
Long term Astro lover here, and I've deployed multiple landing pages using Astro. It's really fast, and awesome!
You might find:
A, B & C
These are all in astro.
There's a previous version of A which was in Astro as well. : )
Also,
That's an editor problem. 🌝
Thanks for sharing those sites! Did you design them? They're absolutely stunning.
It's both an editor problem and a skill issue, and I'll happily admit to that. It could also be a non-problem if the files were named
homepage.astro
,blog-list-page.astro
,blog-detail-page.astro
, etc. I want to live in that world 😁Thank you, the website designs are derived from templates which later I modified. Astro ecosystem has a lot of those templates.
I'd like to say, in open-source all your problems are a PR away. 😂
That's not necessarily true: projects reject PRs all the time if they don't think that a feature aligns with the goals of the project.
That said, if I can find the time I might give it a shot anyway. If I could manually configure routing in Astro then it would be one of my favorite web tools I've ever used.
Is it easy to store secrets to connect to external apis?
Connecting to external APIs shouldn't be too bad: it's a
fetch
oraxios
call away.How you store secrets may depend on how you choose to host Astro. I'm hosting my Astro apps on Cloudflare workers because their free plan is generous for serverless. On Cloudflare, theres a place in their UI to put the secrets, and then you can access them in the app via the Astro context. However, if you're building something that connects to an LLM API, hosting an Astro app on a serverless platform isn't going to make a lot of sense because serverless apps are charged by milliseconds of compute time. You'll spend most of that waiting for the LLM to reply.
If you were to host an Astro app on a Digital Ocean droplet, you could probably just use an env file for secrets. Astro also makes it possible to define type safe environment variables, which looks pretty cool.