Documentation

Astro.js

Astro.js is a modern front-end framework for building static websites and web applications. It allows you to write modular HTML, CSS, and JavaScript components and compile them into a static site.

Astro.js

File Structure

  • src/* - This is where your source code lives
  • public/* - Any files here will be served as-is to the root URL of your site
  • src/assets/* - Any static files that you want to reference in your HTML/JS
  • src/components/* - This is where your shared components (React, Vue, Svelte, etc.) live
  • src/config/* - This is where you can keep configuration files
  • src/content/* - This is where you can keep your content files like markdown for blogs
  • src/layouts/* - This is where your layout components live
  • src/pages/* - Any .astro file in this directory will be built as a page
  • src/styles/* - This is where you can keep your style files
  • src/utils/* - This is where you can keep your utility files
  • astro.config.mjs - Configure Astro
  • package.json - Lists package dependencies and contains other metadata
  • tailwind.config.cjs - Configuration file for Tailwind CSS
  • tsconfig.json - Configuration file for TypeScript
.
└── my-app/
    ├── public/
    │   ├── images/
    │   │   ├── opengraph-image.png
    │   │   └── favicon.ico
    │   └── robots.txt
    ├── src/
    │   ├── assets/
    │   │   └── pokemon.png
    │   ├── conpoments/
    │   │   ├── Button.astro
    │   │   └── Header.tsx
    │   ├── config/
    │   │   └── config.ts
    │   ├── content/
    │   │   ├── blog/
    │   │   │   └── post1.md
    │   │   └── config.ts
    │   ├── layouts/
    │   │   └── layout.astro
    │   ├── pages/
    │   │   ├── blog/
    │   │   │   ├── index.astro
    │   │   │   └── [...slug].astro
    │   │   ├── 404.astro
    │   │   └── index.astro
    │   ├── styles/
    │   │   └── global.css
    │   └── utils/
    │       └── utils.ts
    ├── astro.config.mjs
    ├── package.json
    ├── tailwind.config.cjs
    └── tsconfig.json

Astro Layouts

  • Layout.astro, Layouts in Astro are just regular .astro components that can be reused across different pages
---
import { ViewTransitions } from "astro:transitions";
import "../styles/global.css";

export interface Props {
  title?: string;
  description?: string;
}

const { title, description } = Astro.props;
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
    <meta name="description" content={description} />
    <ViewTransitions />
  </head>
  <body class="bg-background min-h-screen font-sans antialiased">
    <main>
      <slot />
    </main>
  </body>
</html>

Astro Pages

  • Astro pages are .astro files located in the src/pages/ directory
  • Each .astro file corresponds to a route based on its file name
---
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <h1>Hello, world!</h1>
</Layout>

Hydrating Interactive Components

  • client:load - The component will hydrate once the page has loaded
  • client:visible - The component will hydrate once it’s visible in the viewport
  • client:idle - The component will hydrate once the browser is idle
  • client:only - The component will only be sent to the client and won’t render on the server
---
import InteractiveButton from "../components/InteractiveButton.tsx";
import InteractiveCounter from "../components/InteractiveCounter.tsx";
import InteractiveModal from "../components/InteractiveModal.tsx";
---

<InteractiveButton client:load />
<InteractiveButton client:visible />
<InteractiveButton client:only="react" />

Optimized Images

  • Define a collection with a schema
// content/config.ts
import { defineCollection, z } from "astro:content";

// content/blog
const blog = defineCollection({
  schema: ({ image }) =>
    z.object({
      // Other properties
      imageUrl: image().refine((img) => img.width >= 600, {
        message: "Image must be at least 600 px width.",
      }),
    }),
});

export const collections = {
  blog: blog,
};
  • Image component optimizes the image by converting it to the AVIF format
---
import { Image } from "astro:assets";

export interface Props {
  imageUrl: ImageMetadata;
}

const { imageUrl } = Astro.props;
---

<Image
  format="avif"
  src={imageUrl}
  alt=""
  class="aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]"
/>

Markdown Code Block

  • Creates a “Copy” button for each code block in the Markdown content
---
// MarkdownLayout.astro
---

<article class="prose mt-10 lg:prose-xl">
  <slot />
</article>

<script is:inline>
  function main() {
    const codeBlocks = Array.from(document.querySelectorAll("pre"));
    for (const codeBlock of codeBlocks) {
      const wrapper = document.createElement("div");
      wrapper.className = "relative";
      const copyButton = document.createElement("button");
      copyButton.className =
        "absolute top-0 right-0 m-2 rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600";
      copyButton.innerHTML = "Copy";

      const code = codeBlock.querySelector("code");

      if (code && codeBlock && codeBlock.parentNode) {
        codeBlock.appendChild(copyButton);
        codeBlock.parentNode.insertBefore(wrapper, codeBlock);
        wrapper.appendChild(codeBlock);

        copyButton.addEventListener("click", async () => {
          const text = code.innerText;
          await navigator.clipboard.writeText(text).then(() => {
            copyButton.innerText = "Copied";

            setTimeout(() => {
              copyButton.innerText = "Copy";
            }, 1000);
          });
        });
      }
    }
  }

  main();
</script>

Theme

  • Toggle data-theme Attribute
import React, { useEffect, useState } from "react";
import { siteConfig } from "../config/site";

const Header: React.FC = () => {
  const [theme, setTheme] = useState<"night" | "fantasy">("night");

  useEffect(() => {
    const getDefaultTheme = (): "night" | "fantasy" => {
      const storedTheme = localStorage?.getItem("theme");
      if (!storedTheme) return "night";
      if (storedTheme === "night" || storedTheme == "fantasy") {
        return storedTheme;
      } else {
        return "night";
      }
    };
    const defaultTheme = getDefaultTheme();
    setTheme(defaultTheme);
    document.documentElement.setAttribute("data-theme", defaultTheme);
  }, []);

  function handleTheme(theme: "night" | "fantasy") {
    setTheme(theme);
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("theme", theme);
  }
  return (
    <nav className="navbar">
      <div className="navbar-start">
        <a className="btn btn-ghost text-xl normal-case" href="/">
          <span className="sr-only">{siteConfig.name}</span>
          <img className="h-8 w-auto" src={siteConfig.logo} alt="Logo" />
        </a>
      </div>
      <div className="navbar-end">
        <div className={theme === "night" ? "hidden" : ""}>
          <button
            className="btn btn-circle btn-ghost"
            aria-label="hidden"
            onClick={() => handleTheme("night")}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 512 512"
              className="h-5 w-5 fill-current"
            >
              <path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"></path>
            </svg>
          </button>
        </div>
        <div className={theme === "night" ? "" : "hidden"}>
          <button
            className="btn btn-circle btn-ghost"
            aria-label="hidden"
            onClick={() => handleTheme("fantasy")}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 384 512"
              className="h-5 w-5 fill-current"
            >
              <path d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"></path>
            </svg>
          </button>
        </div>
      </div>
    </nav>
  );
};
export default Header;