For a few years, web projects have been taking more and more responsibility, which means more and more JavaScript.
Nowadays, the top SPA Frameworks, React, Vue, or Angular, have great developer experience and allow us to build complex applications but have some pain points.
The size of JavaScript to Browsers.
The SEO doesn't like the SPA.
Each framework tries to solve the problem with bundle strategies, split in modules, lazy loading, and SSR to try to patch the problem. The hydration process is so heavy for the browser, so our issues still need to be solved 100%.
What can we do?
The web is continuously evolving, and some solutions exist to help us with performance and SEO, and today I will talk about Astro.
What is Astro?
Astro is a “static” website generator focusing on performance and SEO, providing interactivity with Astro components or using the power from React Vue or Svelte.
Astro helps us move away from heavy JavaScript Websites but keep the power of use components scalable and maintainable websites.
Astro works with MPA and Island architecture, the old MPA frameworks use server language, but Astro works with Javascript, which allows rendering components on the server and client side.
The Island Architecture means Astro works with small apps or islands to add interactivity, using Astro components or working as a container to add React, Vue, or Solid components.
The Island architecture is not unique to Astro. Other frameworks are, but with other approaches:
Eleventy with Preact: Using the combination of Eleventy and Preact with the
WithHydration
wrapper to hydrate the components on the client.Marko: it ships the interactive component's hydration code to change the browser's state, so the Marko compiler optimizes where it runs, client or server.
We already have an overview of Astro, but the best way to learn is by building something from zero.
The Project
We will build an app and consume the Rick And Morty API. The app shows a list of characters on the home page, click on details and go to the details page when clicking on a single character.
We must create an Astro project with the following:
Components: Header, Navigation, Footer, Character List, Character
Pages: Home and CharacterDetail.
API: Functions to provide the data.
Create The Project
Using the terminal, run the following command npm create astro@latest
. It will trigger an assistant to ask questions and pick Just the basics
. Use Typescript strictly to set everything ready to start.
npm create astro@latest
npx: installed 78 in 8.962s
+—————+ Houston:
| ^ u ^ I'll be your assistant today.
+—————+
astro v1.6.12 Launch sequence initiated.
? Where would you like to create your new project? » astro_rick_morty
The option
Just the basic
creates an essential structure for us. It helps us to explain some points about Astro.
Project Structure
We have the project; let's navigate into the project to see every directory:
public: to store static files like images.
pages: All files in the pages are components and work as routes.
layouts: The components work as containers to reuse sections on all pages.
components: store all components to use in the app.
astro.config
: help us to define other frameworks to use in the app (if we need them)
Delete the generated files Card.astro
, Layout.astro
and index.astro
, to start from zero.
Create the index.astro
file again, run the terminal command, and start the local server in port 3000 with an empty page in the browser.
astro dev
astro v1.6.12 started in 36ms
┃ Local http://127.0.0.1:3000/
┃ Network use --host to expose
The Astro File
Before creating the components, I want to give an overview of the Astro Components sections, the ComponentScript
and ComponentTemplate
.
ComponentScript:
It is the Typescript area defined with ---
a front matter like markdown files and allows import of Astro or framework components declares variables, and functions to use in the ComponentTemplate
.
---
const hello = (name: string) => {
return `Hello${name}!`
}
const title = 'Welcome Astro'
---
Component Template
It renders HTML or imported components and supports Javascript expression and Astro directives.
The Astro Component syntax is a superset of HTML (Like Typescript for Javascript), allowing us JSX-like expression but is not JSX.
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>{title}</h1>
{ hello('Bezael')}
</body>
</html>
We have already finished an overview of Astro components. Let's build our components.
Read more about components in Astro.
Add Tailwind
We want some app styling to easily integrate Astro with a single command and configure everything automatically.
npx astro add tailwind
Read Tailwind integration in Astro docs.
Components
We will start with two simple components the footer
and navigation
, we learn each component has two main areas.
The footer: create a footer.astro
file in the components
directory, and add ComponentScript
area with ---
and create the variable message
with the Build with Love and Astro
. In the template, add a message variable using the {}
:
The final code looks like this:
---
const message = 'Build with Love and Astro'
---
<footer>
{message}
</footer>
The navigation: Similar to the footer, create navigation.astro
and declare an array n the ComponentScript
area. The object for navigation is an array, and the object has two values path
and title
.
const options = [{ path: '/', title: 'Home'}, { path: 'about', title: 'About'}];
Use the map function in the ComponentTemplate
using a JSX-like expression. Add the link element to render the menu properties inside the map. {value}
.
Note: I'm using some tailwind classes just styling.
---
const options = [{ path: '/', title: 'Home'}, { path: 'about', title: 'About'}];
---
<div
class="flex flex-col items-center justify-center space-y-3 md:flex-row md:space-y-0 md:space-x-8 md:justify-end"
>
{options.map((p: any) =>
<div class="group">
<a href={p.path}>{p.title}</a>
<div
class="mx-2 mt-2 duration-500 border-b-2 opacity-0 border-black group-hover:opacity-100"
></div>
</div>
)}
</div>
Layout
The Layouts are components to provide reusable UI; they work as a container for the pages combined with the to give space to inject the pages. (It works like the router-outlet in Angular).
In the layouts, the directory creates the layout.astro
file; in the ComponentScript, import Footer
and Navigation
components.
We added the Navigation and Footer and a slot to Astro inject the pages.
The final code looks like this:
---
import Footer from "../components/footer.astro";
import Navigation from "../components/navigation.astro";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<Navigation></Navigation>
<div class="flex flex-col items-center justify-center min-h-screen m-2 bg-cyan-50">
<slot>
</slot>
<Footer></Footer>
</div>
</body>
</html>
Pages
The pages are components in the pages/
responsible for managing the routing, load data, and page content.
The index.astro
component is the first page; we want to reuse the layout component and import the Layout
component.
Add h1 with Hello Astro
in the Layout body, Astro renders the content in the slot area defined in the layout.
The final code looks like this:
---
import Layout from "../layouts/layout.astro";
---
<Layout>
<h1>
Hello from Astro
</h1>
</Layout>
Save changes, the Hello from Astro
the message is shown with the layout style.
Get Data From API
The CharacterList
and Character
need data to bind to the components to interact, so we are going first to create the functions to get the data
Because I'm an Angular guy, I created the directory services
with the file rickmorty.service.ts
.
The service has five functions:
getAPICharacters: Get the list of all characters from API.
getAPISingleCharacter: Get a single character from ID.
transformData: transform the response and return some properties.
getSingleCharacterDetail: return the Character from ID.
getCharacters: return all Characters.
Because the rickandmorty
API has a structure; it creates an interface for each entity.
export type APICharacterResponse = {
results: APICharacterModel[]
}
export interface APICharacterModel {
id: number;
name: string;
status: string;
species: string;
type: string;
gender: string;
origin: Location;
location: Location;
image: string;
episode: any[];
url: string;
created: Date;
}
export interface Location {
name: string;
url: string;
}
API Calls
I won't go into deep details, but We use the async
await and fetch to get the data.
const getAPICharacters = async (): Promise<APICharacterResponse> => {
const charactersRequest = await fetch(API);
return charactersRequest.json();
}
const getAPISingleCharacter = async (id: string) => {
const request = await fetch(`${API}/${id}`);
return request.json();
}
Because the API Response is a complex object, we create the transformData
method.
const transformData = (characters: APICharacterResponse) => {
return characters.results.map((c) => {
return {
id: c.id,
name: c.name,
image: c.image
}
});
}
Finally, create two functions from the components getCharacters
and getSingleCharacterDetail
. Each one calls a specific method to get the data from API.
export const getCharacters = async () => {
const characters = await getAPICharacters();
return transformData(characters);
}
export const getSingleCharacterDetail = async (id: string) => {
return await getAPISingleCharacter(id);
}
Learn more about async, await, and fetch
Astro.props And Components
We need to create two components and pass data to it:
character: show a character image and name.
characterlist: take the responsibility to list all characters.
To pass the data to the components, we use Astro.props
to read values from component attributes, attributes like:
<my-component title="hi" id=""/>
Create the character.astro
file in the script section, extract to attributes for the component: title
and image
from Astro.props
In the Markup, use the variables as we did before:
The final code looks like this:
---
const { name, image } = Astro.props;
---
<div class="relative group">
<img alt={name} width="100%" height="100%" src={image} />
<div
class="absolute bottom-0 left-0 right-0 p-2 px-4 text-white duration-500 bg-black opacity-0 group-hover:opacity-100 bg-opacity-40"
>
<div class="flex justify-between w-full">
<div class="font-normal">
<p class="text-sm">{name}</p>
</div>
</div>
</div>
</div>
Next, we have to work with the CharacterList
, which uses the Character components, gets a list of characters from the API, and iterates over the array similar to the Navigation
.
First, import the Character component, and from Astro.props
get the character
---
import Character from "./character.astro";
const { characters } = Astro.props;
---
In the Component template, iterate over the character array and render the component.
The final code looks like this:
---
import Character from "./character.astro";
const { characters } = Astro.props;
---
<div class="grid gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{
characters.map((character: any) => (
<Character name={character.name} image={character.image}></Character>
))
}
</div>
Display the Data
We are in the final steps; in this step, we want to show the list of Characters and get the data from API.
Open the index.astro
and import the CharacterList component and the getCharacter function from rickymorty.service
Define the characters
variable to save the data from the getCharacter
function using the await keyword. In the template, pass the characters to the CharacterList
component
The final code looks like this:
---
import Characterlist from "../components/characterlist.astro";
import Layout from "../layouts/layout.astro";
import { getCharacters } from "../services/rickmorty.service";
const characters = await getCharacters();
---
<Layout>
<Characterlist characters={characters}></Characterlist>
</Layout>
Save and see the list of the Characters in the browser.
Perfect! We have the list, but I want to click on one character and see the details.
Passing Parameters
Astro supports static and dynamic parameters and dynamic routes using the file name like characters/[].astro and reading it using Astro.params
For the dynamic parameters, we must use the server()
mode, by default, uses static. To change it, open the astro.config.mjs
add the output in the defineConfig
section to the server:
export default defineConfig({
output: 'server',
integrations: [tailwind()]
});
Next, create a new file in the pages section, like characters/[id].astro
Import the Character component and the getSingleCharacterDetail
function.
Get the id from Astro.params
and pass it to the getSingleCharacterDetail
, and using await, get the response and extract the name and image.
The [id].astro final code looks like this:
---
import Character from "../../components/character.astro";
import Layout from "../../layouts/layout.astro";
import { getSingleCharacterDetail } from "../../services/rickmorty.service";
const { id = '' } = Astro.params;
const {name, image} = await getSingleCharacterDetail(id);
---
<Layout>
<Character name={name} image={image}></Character>
</Layout>
To allow the user to click in detail and navigate. Edit the characterlist.astro
component and wrap the component with an <a
in the href
pass the route characters/${character.id}
The final code looks like this:
---
import Character from "./character.astro";
const { characters } = Astro.props;
---
<div class="grid gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{
characters.map((character: any) => (
<a href={`characters/${character.id}`}>
<Character name={character.name} image={character.image}></Character>
</a>
))
}
</div>
Save and navigate to the character details!
Deploy
We already created a basic app with Astro, but we still need the deployment, and Astro has an easy way to deploy and excellent documentation. My example is using vercel
The Astro team has Adapters
to deploy quickly and configure the astro.config.mjs
with a single command:
npx astro add vercel
Read more about deploy
Recap
We learn a bit about how astro works, and how easy it is to build an app. Astro looks like a great alternative for building static websites using the power of frameworks like React, Vue, or Svelte if we require extra reactivity.
Feel free to see the web or play with the GitHub code.
Top comments (0)