EST. 2005
--:--:-- | GMT+5:30

working with astro framework

I am learning astro. Yk the “insanely fast” framework for server side rendering & almost no .js Code on the client side. Yeah that one. Since I’ve caterpillar’s memory I’ll write a simple blog in case I forget. Note that this is bare bones basic stuff, you should really read the docs if you want to deep dive into the framework itself.

Let’s get into it. The reason astro is a nice framework because it heavily forces island architecture.

The simple definition of islands architecture is, render HTML pages on the server side and inject the “placeholders” around the dynamic region of the HTML page. In the image notice the (server-rendered HTML) sections. These are the “placeholders” that’ll be injected onto the client side, into containerized widgets. And ofc they can be reused by the client. It looks like that it’s micro frontend architecture but it’s not, because the term “micro frontend” doesn’t imply that the dedicated rendered output will be rendered using HTML.

Usually, we might have <script> That looks for a component and instantiates a jQuery plugin on it. But in astro the component would be rendered on the server side and a dedicated <script> Gets emitted for that that loads the dedicated component. (ik its a wrong explanation I’ll fix it later)

The island architecture approach divides each component with it own loading <script> Meaning, it separates the hydration of components resulting in faster and smoother pages unlike progressive hydration which is one single <script> With gradual hydration. (both are very different)

This makes the island architecture very different from progressive hydration. It doesn’t require top down rendering! Since each component is isolated unit one component’s performance issue won’t affect/hold down the other’s.

An island is simply a interactive component that can be independently hydrated or rendered without involing the rest of the page. A client island is an interactive component that is hydrated separately from the rest of the page, whereas, server islands are rendered separately

Client Islands

Astro renders every UI component to just html & css by default, and strips all the js automatically.

And to turn any of the static UI component into an interactive island we just need a client:* directive. After this, Astro builds and bundles your client side js for optimized perf.

// initially: standard build output yields pure html

<button /> // this is just a stripped js, only html & css version of a component!

// with island architecture enabled
<button client:load /> 

// now after using the client:* directive we make it interactive and turn it into an interactive component and of course the rest of the stuff stays static. pretty neat, right?

This interaction thingy is configured at the component level [per the docs], this implies that we can use different directives for different components. client:idle tells a component to load when browser becomes idle. client:visible tells a component to load only once it enters the viewport. and so on.

Server Islands

These exist to move expensive/slow server side code out of the way of the main render process, this makes easy to combine high performance static HTML and dynamic server generated components.

By adding server:defer in the component one can make the component as its own server island. This breaks your page into server rendered areas that load parallely instead of sequentially.

// pages/test.astro
import Component from "../components/Component.astro";

<p>hello world</p> // this portion renders immediately to usr.
<Component server:defer/> // this will render independently as a server island and won't block the rest of html from rendering instantly

The architecture of server islands ensures that the components will be cached very aggressively (ofc it also depends on the server config not just the island). The page displays fallback content instantly and shows the rendered content as soon as it’s available.

Finally Starting to Make Something

So I initially thought I should follow the “blog tutorial” on docs but.. insert I don’t think I will meme

Anyway I’ll be hosting my portfolio on github pages and they really don’t have the concept of server side rendering so server islands won’t be used. Only client stuff. (I may or may not write it)

// src/pages/page.astro
---
// this is the component script section, it runs exclusively on the server during build
import Home from "../../src/layouts/home_page_layout.astro"
---
// your template goes here
<Home>
	<p>hello world</p>
</Home>

The information on top of the .astro file, included in code fences is called the frontmatter. This data is just javascript section. The frontmatter is server only js, and the section below it (template) is the HTML sent to client.

Instead of hardcoding the string into the page, one can use { } for dynamic content rendering.

---
// define javascript variables here at build time
const data = "hey, im stupid";
---
<head>
</head>
<body>
	<!-- inject dynamic variables directly into HTML securely -->
	<h3> { data } </h3> 
</body>

Pretty neat.

I stole most of the design ideas from namishh.com and avhi.in , so credits to them for the inspiration. Anyways, I was pretty confused about where to place the navigation component. Initially it was above the footer, but that felt wrong (because it is).

The top section of the site was basically “EST. 2005” and a clock component, followed by a profile picture. This whole thing was hardcoded into index.astro, but there was an issue.

If I want to place the navigation component below the profile picture, I need to ensure that the stuff above it stays the same on every page, while the rest is the content that actually changes.

That’s why I removed the hardcoded part and made the whole thing a <Header /> Component. (you can see my trash frontend skills here).

Rest of the stuff was basic html, css, and structuring thingy. One more thing I want to talk about is props.

In astro user can implement dyanamic behvaiour to the components such that for each new instance of the component the only prerequisite is the data required to inject into the component. And everything else works just fine! Define a layout for placement of the components/ instances and voila!

Think of astro components as functions and the props as arguments for the function. In my site I was making each “project” as a card with it’s title, description, tags, and optional url. So in the component’s definition we do:

// destructing the injected data from astro.props
const { title, description, tags, link } = Astro.props;

This catches the bundled data and unpacks it into the variables that we can later use in html with { } Syntax.

Fun fact: in type script one can define interface Props That’ll work like type checker and enforce the data definition.

After this in my projects.astro page I just do

// mapping over a list of projects and rendering a component for each
{ 
	projects.map(p => (
	<BoxComponent 
		title = { p.title }
		description = { p.description }
		tags = { p.tags }
	 />
	))
}

And we’re good to go.

Content Collections

It’s a method to manage sets of content in an Astro project. Like blog and stuff. These help organize and query the docs, enable intellisense and type checks. Since I have sections like notes, writings, arts I can use this to structure my stuff in the src/content/ dir.

There are two types of content collections available to allow you to work with data fetched… either at build time or at request time!

Both of these use:

  1. A required loader that retrieves the content and metadata from wherever it’s stored through content focus APIs.
  2. An optional collection schema that allows you to define the expected shape of each entry for type safety… kinda like zod. But I don’t think I’ll use it or I might.

Collections stored locally can use build time loaders like glob(), file() To fetch from .md, .mdx, .yaml, .json

// Example: src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const notesCollection = defineCollection({
	// using the glob loader to fetch all mdx files from our folder!
	loader: glob({ pattern: "**/*.mdx", base: "./src/content/notes" }),
	schema: z.object({
		title: z.string(),
		date: z.date(),
	}),
});

export const collections = { notes: notesCollection };

Or you can use custom build time loaders. I don’t think I’ll look into it for now. But I might migrate my content/ To it later.

Dynamic Content Loading

In dynamic content loading, you predefine a file in pages/ route with file name [slug].astro where slug is the identifier (usually derived from the file name / path) of the content you’ve to render.

Anyways after some tinkering around I ended up with this structure

src/
├── content.config.ts
├── env.d.ts

├── assets/
│   ├── astro.svg
│   ├── background.svg
│   ├── clock.svg
│   └── fonts/

├── components/
│   ├── artbox.astro
│   ├── cleanindex.astro
│   ├── clock.astro
│   ├── footer.astro
│   ├── header.astro
│   ├── navigation.astro
│   ├── post.astro
│   ├── projectbox.astro
│   ├── socials.astro
│   └── webring.astro

├── content/
│   ├── arts/
│   ├── consumes/
│   ├── notes/
│   └── writings/

├── layouts/
│   └── home.astro

├── pages/
│   ├── arts.astro
│   ├── index.astro
│   ├── notes.astro
│   ├── projects.astro
│   ├── writings.astro
│   ├── rss.xml.js
│   └── content/
│       ├── arts/
│       │   └── [slug].astro
│       ├── consumes/
│       │   └── [slug].astro
│       ├── notes/
│       │   └── [slug].astro
│       └── writings/
│           └── [slug].astro

└── styles/
    └── global.css

Pretty nice huh?

So I’ll try to explain the whole dynamic content rendering pipeline.

It all begins with user wanting to read the notes route on pages/.
That triggers the getCollection('notes') method to fetch an array
containing all the collection entries (basically all the posts in notes/ dir).
The data in these entries is used to create a CleanIndex component that
renders index of all the posts within notes/ (similar to how a book
has index on first page).
We pass a slug argument in this component in order
to attach the url via href property of an <a> tag.
Now we’ve got our route set but
if Astro only treats stuff in pages/ as routes… how do we actually
get it to show? I mean we never added anything in pages/ right?
It’s all in src/content/

Very simple solution is just drag and drop content/ in the pages/ but
I was learning so I had to overengineer this part. (Also on a big scale
this isn’t a very logical thing to do.)

That’s where our dynamic content loading comes in.
We predefine a [slug].astro file in the exact dir/ format
in our case pages/content/notes/[slug].astro.
Whenever user clicks the post, dynamic routing gets triggered in
/page/content/notes/[slug].astro.
This file exports a getStaticPaths() method which returns an array of
objects that define the route params (not the content itself) that need to be generated.
After that, inside the page, you fetch the actual content using
getEntry('notes', slug) (or getCollection() if needed).

And then calling .render() on the entry compiles the mdx into a Content component.
This Content is not raw data but a renderable component
containing the compiled output of the post…
And of course this component accepts an argument components which can be
used to configure the rendering style of .mdx.
Now don’t get me wrong you can absolutely just do this using .css
in the rendered section directly but this is a much cleaner / better
approach…

is:inline Tag

One thing that bothered me was the clock component in my header.
It worked very nicely sure but there was a small bug. Whenever the site’s
loaded for a very small time frame you’ll see the placeholder text instead
of the clock itself. This was very irritating because the flick really
annoys ppl.

So the solution was is:inline. When loading scripts, Astro usually bundles
and processes them (and often defers execution), and that is the problem right there.
During that delay we briefly see the placeholder.
The is:inline directive simply skips that processing step and keeps the script
inline in the HTML, so it runs immediately as the page loads and voila!

<head>
	<!-- using is:inline tells astro not to bundle this script, it executes instantly the moment the browser reads this line -->
	<script is:inline>
		console.log("i execute immediately and fix the clock flicker!");
	</script>
</head>

Now we’re going to move towards customizing .mdx files and how we can create & pass custom components in it. One fairly nice thing I noticed was that if we are using content collections we can configure our define collection module such that .md files within a specific collection gets treated as .mdx! It won’t apply this behaviour globally… but only on the collection we choose!

import { defineCollection, z } from 'astro:content';

const blogs = defineCollection({
	schema: z.object({ title: z.string() }),
	markdown: {
		// treat each pure .md file in this specific collection as full mdx!
		format: 'mdx', 
	}
})

Using Custom Components in Astro

There are two primary ways to do this.

  1. Create your own jsx components like we did in pages/ and then import those components in .mdx file, use it and we’re good to go.
  2. Customize predefined stuff in .mdx Only. For example, code blocks, headings, hyperlinks, etc.

I don’t think I want custom jsx components for now so I’ll go with overriding the mardown generated elements. …

That’s it. After this it’s just me using antigravity and fixing spelling mistakes in blogs and changing theme colors.