All Posts(11)

Page 1 of 3
Cool pic

Pagefind

Search your site

Pagefind is a search library for static websites. It works by indexing the built static files of our site, and adding a JavaScript search API that allows the user to find content with code that runs client-side.

NOTE

Pagefind can be used with Hugo, Eleventy, Jekyll, Next, Astro, SvelteKit, or any other SSG. There’s even an Astro integration, but we’ll take care of it ourselves, from scratch (it’s more fun and educative).

Installing Pagefind

Pagefind is a static binary, written in Rust with no dynamic dependencies, so installing it would be as simple as:

  1. Downloading the release for our platform (currently supported on Windows, macOS, and Linux distributions).
  2. Place it somewhere in our PATH
  3. And voila!

TIP

The official Pagefind site contains excellent and comprehensive instructions for installing Pagefind

Since we are in Node.js, we can take advantage of the wrapper package for this runtime, and run:

npx pagefind --site "public"

Where public is whatever folder your SSG outputs the generated site, but I’m gonna go ahead and add it to my project dependencies so that I can integrate it in my package.json scripts:

npm i pagefind

Building the Index

As we’ve mentioned at the beginning, Pagefind indexes your site after it builds, so let’s build it:

npm run build

That should output our site’s build to a folder named dist. Now let’s use the pagefind command to index that folder:

pagefind --site dist

That will generate a folder named pagefind under our dist folder (so in dist/pagefind) that contains:

  • The indexed content.
  • The JavaScript that the user’s browser will need to search the indexed stuff.
  • Some UI scripts and stylesheets.

TIP

Probably a good idea to add a pagefind entry to your .gitignore; I added also another one for the dist folder.

Updating our build script

So far we need to use several commands to get Pagefind generating an index of the latest version our our site:

  1. Build the site itself: npm run build
  2. Generating the Pagefind index: pagefind --site dist

It’s probably a good idea to combine these two commands, so that when we deploy our site, whatever pipeline runs the npm run build, will also generate the Pagefind stuff. So let’s update the build script in our package.json file:

"scripts": {
"build": "astro build && pagefind --site dist"
}

Running npm run build now, would produce the usual output, plus the following:

[...]
dist/pagefind
[Building search indexes]
Total:
Indexed 1 language
Indexed 45 pages
Indexed 1285 words
Indexed 0 filters
Indexed 0 sorts

If we open the dist folder (where Astro outputs the build), we should see a pagefind folder with our indexed site.

CAUTION

In my case, that didn’t work due to the way my github workflow was written:

- name: Build with Astro
run: |
${{ steps.detect-package-manager.outputs.runner }} astro build \
--site "${{ steps.pages.outputs.origin }}" \
--base "${{ steps.pages.outputs.base_path }}"
working-directory: ${{ env.BUILD_PATH }}

As you can see, it runs the astro command directly, and not our npm run build. So if you don’t want to mess with your pipeline (no biggie, but hey), you can add the following to your build script:

"scripts": {
"pagefind": "pagefind --site dist && cp -r dist/pagefind public",
"build": "astro build && npm run pagefind"
}

That way, after every build, we end up with the pagefind folder in our public directory, and our GitHub pages deployment won’t have trouble finding it.

Bottom line, you can do it your way, but the whole setup needs the pagefind folder available in public folder.

IMPORTANT

Don’t forget to remove pagefind from your .gitignore file.

Adding the Component UI

Assuming that everytime we build our site, Pagefind indexes its content, we need a UI to search and see the results. Pagefind now recommends the Component UI, which gives us declarative web components for common search interfaces.

NOTE

The previous UI, based on pagefind-ui.js and new PagefindUI(...), still exists in the docs, but the Component UI is the recommended path now.

The nice thing about the new system is that the components follow WAI-ARIA best practices, handle visible and assistive text, and are designed to be hard to misuse.

Loading the Component UI

For the Component UI, we load these files:

<link href="/pagefind/pagefind-component-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-component-ui.js" type="module"></script>

In Astro, I added them to my main layout and used import.meta.env.BASE_URL so the paths still work if the site ever lives under a subpath:

---
const pagefindPath = `${import.meta.env.BASE_URL}pagefind/`
---
<link href={`${pagefindPath}pagefind-component-ui.css`} rel="stylesheet" />
<script src={`${pagefindPath}pagefind-component-ui.js`} type="module"></script>

Selecting the Component

After loading the Component UI script and stylesheet, we can choose between a couple of components:

  • Modal search: good when you want a search dialog that opens from the navbar.
  • Searchbox: good when you want a search input with dropdown results.
  • Build your own: good when you want to assemble the input, summary, filters, and results yourself.

For my site, the modal is the best fit:

<pagefind-modal-trigger></pagefind-modal-trigger>
<pagefind-modal></pagefind-modal>

With the modal, Pagefind handles the dialog, focus management, keyboard navigation, and the Cmd+K / Ctrl+K shortcut for us.

Replacing the Custom Search Modal

Before, I had my own search button, my own overlay, and some JavaScript to open and close it. With the Component UI, the markup is enough:

---
const pagefindPath = `${import.meta.env.BASE_URL}pagefind/`
---
<div class="pagefind-search" data-pf-theme="dark">
<pagefind-config bundle-path={pagefindPath}></pagefind-config>
<pagefind-modal-trigger placeholder="Search"></pagefind-modal-trigger>
<pagefind-modal reset-on-close>
<pagefind-modal-header>
<pagefind-input></pagefind-input>
</pagefind-modal-header>
<pagefind-modal-body>
<pagefind-summary></pagefind-summary>
<pagefind-results show-images></pagefind-results>
</pagefind-modal-body>
<pagefind-modal-footer>
<pagefind-keyboard-hints></pagefind-keyboard-hints>
</pagefind-modal-footer>
</pagefind-modal>
</div>

The minimal version would be even smaller:

<pagefind-modal-trigger></pagefind-modal-trigger>
<pagefind-modal></pagefind-modal>

I used the longer version because I wanted to keep result images enabled:

<pagefind-results show-images></pagefind-results>

That’s the Component UI way to enable images in search results.

The pagefind-config component tells the UI where the generated Pagefind files live:

<pagefind-config bundle-path="/pagefind/"></pagefind-config>

In my Astro component, that value comes from BASE_URL:

<pagefind-config bundle-path={pagefindPath}></pagefind-config>

Keeping the Content Indexed

The UI is separate from the indexing markup. My post and project pages still mark their main content with data-pagefind-body:

<div class="post-content" data-pagefind-body>
<Content />
</div>

That tells Pagefind which part of the page should be searchable.

Result Images

For images in search results, I provide explicit Pagefind metadata in the page head:

<meta
data-pagefind-meta="image[content]"
content={post.data.coverImage.cropped.src}
/>
<meta
data-pagefind-meta="image_alt[content]"
content={post.data.coverImage.alt}
/>

One thing to keep in mind is that Pagefind indexes the built site. If images look broken while running the dev server, test with the production build instead:

Terminal window
npm run build
npm run preview

Fixing Result Titles

My search results showed the site title instead of the post title. The reason was subtle: Pagefind’s automatic title metadata comes from the first h1 on the page.

My logo used to be an h1:

<h1 class="anaglyph">生活のバランス</h1>

So Pagefind treated the logo as the page title. The fix was to make the logo a non-heading element:

<span class="anaglyph">生活のバランス</span>

Then I restored the old visual size with CSS:

.anaglyph {
display: inline-block;
font-size: 2em;
font-weight: 900;
line-height: 1.2;
}

Another good option is to explicitly mark the real page title for Pagefind:

<h1 data-pagefind-meta="title">{post.data.title}</h1>

That tells Pagefind exactly what title to use in search results.

Theming the New UI

The Component UI uses CSS custom properties. I scoped them to my search wrapper:

.pagefind-search {
--pf-text: var(--col-text-1);
--pf-text-secondary: var(--col-text-2);
--pf-text-muted: var(--col-text-2);
--pf-background: var(--col-bg);
--pf-border: var(--col-text-3);
--pf-border-focus: var(--col-text-2);
--pf-hover: var(--col-overlay);
--pf-outline-focus: var(--col-accent-1);
--pf-font: 'Mona Sans', sans-serif;
--pf-input-height: 2rem;
--pf-input-font-size: 16px;
--pf-border-radius: 0.3rem;
--pf-modal-backdrop: var(--col-overlay);
--pf-modal-max-width: min(800px, calc(100vw - 2rem));
--pf-modal-max-height: min(80dvh, 800px);
--pf-modal-top: 5rem;
}

This made the Pagefind modal fit the rest of the site’s dark theme.

Fixing Astro ClientRouter Navigation

One issue I ran into was related to Astro’s client-side navigation. The modal worked the first time, but after clicking a search result and navigating to another page, Cmd+K started behaving erratically.

The browser complained with something like:

Failed to execute 'showModal' on 'HTMLDialogElement':
The element is not in a Document.

The problem was that Astro swapped the page DOM, but Pagefind still had references to the old modal internally. So the next time I opened search, Pagefind sometimes tried to open a modal that was no longer in the document.

The fix was to clean up Pagefind before Astro swaps pages:

<script>
if (!document.documentElement.dataset.pagefindCleanupBound) {
document.documentElement.dataset.pagefindCleanupBound = 'true'
document.addEventListener('astro:before-swap', () => {
document.querySelectorAll('pagefind-modal').forEach((modal) => {
if ('close' in modal && typeof modal.close === 'function') {
modal.close()
}
})
const manager = window.PagefindComponents?.getInstanceManager?.()
if (!manager?.getInstanceNames || !manager?.removeInstance) return
for (const name of manager.getInstanceNames()) {
manager.removeInstance(name)
}
})
}
</script>

That closes any open Pagefind modal and clears Pagefind’s cached component instances before Astro swaps the page. The next page then creates fresh modal and trigger references.

Final Check

After adding the UI, rebuild the site:

Terminal window
npm run build

Pagefind should scan the built HTML, find the data-pagefind-body elements, and generate the index. After that, the search trigger should open the modal, Cmd+K / Ctrl+K should work, and the results should show the correct post titles.

Cool pic

Garage

An S3 object store so reliable you can run it outside datacenters

Most web apps eventually need to handle file uploads — profile pictures, attachments, assets. The instinct is to just write them to disk, but that breaks the moment your app restarts or scales.

Object Storage

Object storage (also called blob storage) is a way of storing data where each piece of data is kept as an independent object rather than inside a traditional file hierarchy or fixed disk blocks. Each object contains the data itself, some metadata describing it, and a unique identifier used to retrieve it.

My Use Case

Sometimes apps run on infrastructure where disk persistence isn’t reliable. That’s the case with Next.js apps, where local filesystem storage is ephemeral — files can disappear after restarts, redeploys, or when the app moves between instances. I needed an object storage solution that matches the production environment.

Why Garage

Garage is a lightweight, self-hosted S3-compatible store that gives you that — without handing your data to a third party. The project is not even hosted in GitHub but in deuxfleurs, which is the collective/organization behind Garage.

NOTE

deuxfleurs (aka 🌼🌼) is a French non-profit that builds decentralized, self-hosted infrastructure tools. Garage was originally built to power their own infrastructure and then open-sourced.

Using something like Garage (or MinIO) makes sense if:

  • You want full control over where data lives — no third party
  • You’re building for a client with data residency requirements
  • You’re deploying the whole stack on your own infra (VPS, bare metal) and want storage there too, not on AWS
  • You want prod to also be self-hosted — in which case Garage in dev mirrors prod exactly

Set Up a Garage Container

You can get familiar with several ways of getting Garage up and running in their quick start. We’ll be using a Docker container, so let’s add the following to our docker-compose.yml file:

services:
# Other services
garage:
image: dxflrs/garage:b72b090a097c8ee2711c8fb065d250ed68dcd0bf
container_name: garage_s3
ports:
- '3900:3900'
- '3901:3901'
environment:
- GARAGE_ALLOW_WORLD_READABLE=1
volumes:
- ./garage_config/garage.toml:/etc/garage.toml
- ./garage_metadata:/var/lib/garage/meta
- ./garage_data:/var/lib/garage/data

Garage Configuration

According to the aforementioned quick start, we need to create a configuration file:

# metadata_dir = "/tmp/meta"
# data_dir = "/tmp/data"
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
# rpc_secret = "$(openssl rand -hex 32)"
rpc_secret = "b89a46551da109ca0ecac670ae2dff0482a1ab98aa3ec28a96f1c65a656dee34"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage.localhost"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage.localhost"
index = "index.html"
[k2v_api]
api_bind_addr = "[::]:3904"
[admin]
api_bind_addr = "[::]:3903"
# admin_token = "$(openssl rand -base64 32)"
admin_token = "zNZmDBE9Tiqs8CaUmN90Loo/63T+c32x0xC8M0Rjk8U="
# metrics_token = "$(openssl rand -base64 32)"
metrics_token = "69VTAfJSVuaog72X8oxcReRFAQKJJelF4BJzQgeXjvk="

IMPORTANT

This file is mounted as a volume in our Docker setup; the default location where garage looks up this file within the container is /etc/garage.toml.

To get the values for some configuration options, we have to run in the terminal the following commands:

  • openssl rand -hex 32 and paste the output in rpc_secret
  • openssl rand -base64 32 and paste the output in admin_token
  • openssl rand -base64 32 and paste the output in metrics_token

Creating a Cluster Layout

Creating a cluster layout for a Garage deployment means:

  1. Informing Garage of the disk space available on each node of the cluster (-c)
  2. As well as the name of the zone (e.g. datacenter), each machine is located in (-z).

Copy Node ID Prefix

Let’s start Garage with docker compose up garage, and check the status with:

Terminal window
docker exec -it garage_s3 /garage status
INFO garage_net::netapp: Connected to 127.0.0.1:3901, negotiating handshake...
INFO garage_net::netapp: Connection established to 2778c9ed427e0ca2
==== HEALTHY NODES ====
ID Hostname Address Tags Zone Capacity DataAvail
2778c9ed427e0ca2 0d45c890ad78 127.0.0.1:3901 NO ROLE ASSIGNED

Then let’s copy the prefix of the node, e.g. 2778 from 2778c9ed427e0ca2, and paste it in <NODE_ID_PREFIX>:

Terminal window
docker exec -it garage_s3 /garage layout assign -z dc1 -c 1G <NODE_ID_PREFIX>

Then apply the layout:

Terminal window
docker exec -it garage_s3 /garage layout apply --version 1

Which should output:

==== COMPUTATION OF A NEW PARTITION ASSIGNATION ====
Partitions are replicated 1 times on at least 1 distinct zones.
Optimal partition size: 3.9 MB
Usable capacity / total cluster capacity: 1000.0 MB / 1000.0 MB (100.0 %)
Effective capacity (replication factor 1): 1000.0 MB
dc1 Tags Partitions Capacity Usable capacity
2778c9ed427e0ca2 256 (256 new) 1000.0 MB 1000.0 MB (100.0%)
TOTAL 256 (256 unique) 1000.0 MB 1000.0 MB (100.0%)
New cluster layout with updated role assignment has been applied in cluster.
Data will now be moved around between nodes accordingly.

Create Bucket

Create bucket, which is the place where our image files will live:

Terminal window
docker exec -it garage_s3 /garage bucket create app-uploads

NOTE

You should get the output: Bucket app-uploads was created.

Add the bucket name to the .env file:

S3_BUCKET=app-uploads

Create Key

Let’s create the key that we’ll use to access the bucket we just created:

Terminal window
docker exec -it garage_s3 /garage key create next-demo-local

Which should output the keys:

Key name: next-demo-local
Key ID: GKbccef323a6f4dedd82f85d59
Secret key: 5ed99b90d1f7f0bd766755cfc3a9daad89ed747fb058dd66c502e544a3ed8f3c
Can create buckets: false
Key-specific bucket aliases:
Authorized buckets:

And paste these values in our .env file:

S3_ACCESS_KEY=GKbccef323a6f4dedd82f85d59
S3_SECRET_KEY=5ed99b90d1f7f0bd766755cfc3a9daad89ed747fb058dd66c502e544a3ed8f3c

Associate Key and Bucket

We need to grant the key access to the bucket:

Terminal window
docker exec -it garage_s3 /garage bucket allow --read --write --owner app-uploads --key next-demo-local

The command above should output something like:

New permissions for GKbccef323a6f4dedd82f85d59 on app-uploads: read true, write true, owner true.

It’s a good idea to verify with this:

Terminal window
docker exec -it garage_s3 /garage key info next-demo-local

Which should output:

Key name: next-demo-local
Key ID: GKbccef323a6f4dedd82f85d59
Secret key: (redacted)
Can create buckets: false
Key-specific bucket aliases:
Authorized buckets:
RWO app-uploads dfc275695b62a17e

NOTE

Note that Can create buckets is false, which means that this key cannot create new buckets, but it can still fully use the buckets explicitly granted to it, in this case app-uploads

Serving Files

Garage in this setup does not allow anonymous object reads, so a raw object URL like http://localhost:3900/app-uploads/events/... cannot be rendered directly in the browser.

NOTE

Whas is an Anonymous Object Read

Imagine you paste http://localhost:3900/app-uploads/events/a.png in browser:

  • Browser sends no S3 auth key/signature.
  • If storage still returns file, that’s an anonymous read. In our setup, Garage rejects that (AccessDenied), so objects are private by default. That’s why our app has to fetch files with credentials server-side (or use signed URLs).

To solve this, we expose a Next.js route handler at app/api/files/[...key]/route.ts. How it works:

  1. The events API returns Mongo data and sets image to a local app URL (for example: /api/files/events/my-image.png).
  2. The browser requests that local URL.
  3. The file route reconstructs the object key from the path (events/my-image.png).
  4. The route fetches the object from Garage using server-side credentials (s3Client + GetObjectCommand).
  5. The route streams bytes back with Content-Type, so the browser can display the image normally.

Why we need it:

  • Keeps Garage bucket private (no anonymous public reads).
  • Browser never needs storage credentials.
  • Works the same way in dev and prod: app API mediates access to object storage.
Cool Pic

Microsandbox

Caging the Autocomplete Beast

I wasn’t very happy with my current sandboxing solution (E2B), so decided to give a try to MicroSandbox, and was glad I switched.

What are sandboxes for?

We’re entering an era where AI agents don’t just suggest code, they write it and execute it. They also install packages, run scripts, inspect files, call CLIs, start dev servers, and sometimes touch code we barely understand yet.

That changes the infrastructure we need around development.

If an agent is going to run arbitrary commands, it needs a safe place to do it. Not my laptop. Not my production machine. Not a long-lived environment full of secrets. It needs a disposable, isolated computer where mistakes are contained and experiments are cheap.

That’s what sandboxes are for.

A good sandbox gives each agent its own environment: a filesystem, a shell, network boundaries, resource limits, and a way to run and inspect code without trusting it too much. For AI coding tools, this is becoming table stakes.

The Microsandbox CLI

Before using the Microsandbox SDK, it’s worth getting familiar with what Microsandbox can do through the Microsandbox CLI.

In this post we’ll look into how to:

  • Install the msb CLI.
  • Create a persistent Node-based sandbox.
  • Execute shell commands inside the sandbox.
  • Install runtime tools and project dependencies.
  • Create and run an Astro app inside the sandbox.
  • Expose the Astro dev server through a mapped local port.
  • Inspect, stop, restart, and remove the sandbox.

Installation

We can install the CLI globally, by downloading and running the following script:

Terminal window
curl -fsSL https://install.microsandbox.dev | sh

WARNING

microsandbox requires Linux with KVM enabled, or macOS with Apple Silicon (M-series chip). Both use hardware virtualization.

After the installation, you’ll have to source ~/.zshrc, then verify the installation:

Terminal window
msb --version
msb 0.4.6

Using the msb CLI

In this section we’ll demo how to create a sandbox named astro-demo, which is nothing but a cointainerized Astro app, running in a Microsandbox.

1. Create A Persistent Node Sandbox

Let’s use the msb CLI to create a named sandbox from the node:22.22.3-slim Docker image and publish Astro’s default dev port:

Terminal window
msb run \
--name astro-demo \
--detach \
--replace \
-m 2G \
-c 2 \
-p 4324:4321 \
node:22.22.3-slim

Several things going on in the command above:

  • The --name flag will name our persistent sandbox as astro-demo.
  • The --detach flag to run the sandbox in the background and returns the terminal inmediately.
  • The --replace flag will recreate the sandbox if it already exists.
  • The -m 2G flag sets 2GB of memory.
  • The -c 2 flag sets 2 virtual CPUs.
  • The -p 4324:4321 is mapping ports from host 4324 -> sandbox 4321.
  • We’re using the node:22.22.3-slim Docker image, which gives us Node.js preinstalled, based on Debian slim. The slim variant is smaller than the full Node image, but still supports apt-get, so we can install tools like git and curl.

2. Prepare The Runtime

Now that our sandbox is running, we can install tools into it:

Terminal window
msb exec astro-demo -- sh -c "apt-get update && apt-get install -y git curl"

Enable pnpm as our package manager:

Terminal window
msb exec astro-demo -- corepack enable
msb exec astro-demo -- corepack prepare pnpm@latest --activate

Verify that everything was successfully installed by checking their versions:

Terminal window
msb exec astro-demo -- node --version
msb exec astro-demo -- pnpm --version
msb exec astro-demo -- git --version

3. Create An Astro App

Let’s create the app inside /home/workspace:

Terminal window
msb exec astro-demo -- sh -c "mkdir -p /home/workspace && cd /home/workspace && pnpm create astro@latest . --template basics --no-install --yes"

The command above will create without installing dependencies. Let’s install them:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && pnpm install --dangerously-allow-all-builds"

TIP

The --dangerously-allow-all-builds flag is for pnpm install. It allows packages with build/postinstall scripts to run during installation.

We use it because generated frontend apps often depend on packages that need install-time build steps. Without it, pnpm may skip those scripts and later commands can fail with ERR_PNPM_IGNORED_BUILDS.

Prefer running it in a separate install step:

Terminal window
pnpm install --dangerously-allow-all-builds

instead of passing it through pnpm create, where it may not be forwarded reliably.

If pnpm create astro prompts unexpectedly, use npm instead:

Terminal window
msb exec astro-demo -- sh -c "mkdir -p /home/workspace && cd /home/workspace && npm create astro@latest . -- --template basics --install --yes"

4. Start The Astro Dev Server

Ok, let’s start the Astro dev server on all interfaces so the published port can reach it:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && pnpm dev --host 0.0.0.0 --port 4321"

The command above also starts the sandbox if it was stopped. Now the sandboxed app should be available at https://localhost:4324 👍

TIP

You can start/stop the sandbox at anytime with:

Terminal window
msb stop astro-demo # Stop
msb start astro-demo # Start

Then inspect the logs:

Terminal window
msb logs astro-demo

NOTE

To start the Astro dev server in the background, we need to background the host-side msb exec process:

Terminal window
nohup msb exec astro-demo -- sh -c "cd /home/workspace && pnpm dev --host 0.0.0.0 --port 4321" </dev/null >/dev/null 2>&1 &
disown

Oof, lot to unpack:

  • nohup keeps msb exec alive after shell hangups.
  • </dev/null detaches stdin.
  • >/dev/null 2>&1 detaches output
  • & backgrounds the host msb exec process (outside the "")
  • disown removes it from shell job control.

To stop the background process, find it with:

Terminal window
ps aux | grep "msb exec astro-demo"

Then kill the matching PID:

Terminal window
kill <PID>

It makes things quite difficult if you’re not used to this level of shell-fu, so nothing wrong if you prefer to run it in the foreground, and open new terminal tabs.

5. Run Commands Inside The Sandbox

Using msb we can inspect files within the sandbox:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && ls -la"

Read package metadata:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && cat package.json"

Run a build:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && pnpm build"

6. Test Environment Variables

Run a one-off command with an env var:

Terminal window
msb exec astro-demo -- sh -c "DEMO_ENV=hello node -e 'console.log(process.env.DEMO_ENV)'"

For app runtime env vars, start the dev server with env in the shell command:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && PUBLIC_DEMO_VALUE=hello pnpm dev --host 0.0.0.0 > /tmp/astro.log 2>&1 &"

7. Inspect Sandbox State

The msb CLI offers a bunch of commands for managing sandboxes. We can list them:

Terminal window
msb ls

Show the ones that are running:

Terminal window
msb ps

Inspect a particular sandbox:

Terminal window
msb inspect astro-demo

Show its metrics:

Terminal window
msb metrics astro-demo

Show its logs:

Terminal window
msb logs astro-demo

8. Stop And Resume

To start (or restart) an existing sandbox:

Terminal window
msb start astro-demo

To stop it:

Terminal window
msb stop astro-demo

9. Clean Up

Whenever we are done with a sandbox, we can stop it:

Terminal window
msb stop astro-demo

And remove it:

Terminal window
msb rm astro-demo

Huzzah!

We’ve played a bit with the CLI, now go have fun with the SDK.

Cool image

Astro collections

Best way to manage your content in Astro

Content collections are the best way to manage content in any Astro project. Using the Content layer API we can define a collection to provide automatic TypeScript type-safety for all of our content.

What are Content Collections?

Let’s say we want to build our personal blog using Astro; using a collection we can turn a bunch of Markdown files (or MDX, Markdoc, YAML, or JSON files) stored locally in our project as the source of our blog content.

NOTE

This API allows us to load local content (aka files in our filesystem), but there are also third-party loaders (our create our own custom loader) to fetch the content from remote sources (think headless CMS).

Defining a Collection

To define a collection, we must create a src/content.config.ts file in your project, which Astro will use to configure your content collections (we can define many collections).

src/content.config.ts
import { z, defineCollection } from 'astro:content'
// Create the collection
export const posts = defineCollection({
// type: 'content', // CAN'T USE THIS WITH LOADERS!
loader: glob({ pattern: ['!_**/*.md','**/*.md'], base: './posts' }),
schema: ({ image }) =>
z.object({
title: z.string(),
author: z.string(),
tags: z.array(z.string()),
description: z.string(),
date: z.date(),
}),
})
// Export the collection
export const collections = {
posts,
}

This file used to go in src/content/config.ts (the old location, Astro API moves fast), but apparently, now it has to be placed in src/content.config.ts. The glob function allows us to do two things:

  • Set the folder for our collection, using base (relative to project root).
  • Exclude some folder/files from being parsed, using the pattern option (!_**/*.md won’t match folders starting with un underscore)

NOTE

We are using TypeScript file (*.ts) to configure our collection, but it’s also possible to use JavaScript (with the .js extension) to define our collection; or even a Michael Jackson file (.mjs).

link test