CtrlK
BlogDocsLog inGet started
Tessl Logo

astro-project-starter

Scaffold an Astro 5.x project with content collections, islands architecture, framework integrations (React/Vue/Svelte), MDX, view transitions, and multi-adapter deployment.

84

Quality

80%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./frontend/astro-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Astro Project Starter

Scaffold an Astro 5.x project with content collections, islands architecture, framework integrations (React/Vue/Svelte), MDX, view transitions, and multi-adapter deployment.

Prerequisites

  • Node.js >= 20.x
  • npm >= 10.x (or pnpm >= 9.x)
  • Git

Scaffold Command

npm create astro@latest my-app -- --template basics --typescript strict
cd my-app

# Add integrations as needed
npx astro add react            # React islands
npx astro add vue              # Vue islands
npx astro add svelte           # Svelte islands
npx astro add mdx              # MDX support
npx astro add tailwind         # Tailwind CSS v4

Project Structure

src/
├── pages/
│   ├── index.astro            # Home page (/)
│   ├── about.astro            # /about
│   ├── blog/
│   │   ├── index.astro        # /blog (list page)
│   │   └── [...slug].astro    # /blog/:slug (dynamic from content collection)
│   └── api/
│       └── health.ts          # GET /api/health (API endpoint)
├── layouts/
│   ├── BaseLayout.astro       # Root HTML layout
│   └── BlogLayout.astro       # Blog post layout
├── components/
│   ├── Header.astro           # Static Astro component
│   ├── Footer.astro
│   ├── react/                 # React island components
│   │   ├── Counter.tsx
│   │   └── SearchWidget.tsx
│   ├── vue/                   # Vue island components
│   │   └── ContactForm.vue
│   └── svelte/                # Svelte island components
│       └── ThemeToggle.svelte
├── content/
│   ├── blog/                  # Blog content collection
│   │   ├── first-post.md
│   │   ├── second-post.mdx
│   │   └── third-post.md
│   └── authors/               # Authors content collection
│       └── jane.json
├── content.config.ts          # Content collection schemas
├── assets/                    # Optimized assets (images processed by Astro)
├── styles/
│   └── global.css             # Global CSS / Tailwind entry
└── env.d.ts                   # Environment type declarations
public/                        # Static files served at / (not processed)
astro.config.mjs               # Astro configuration
.env.example                   # Required env vars template

Key Conventions

  • Static by default: Astro generates static HTML. Pages ship zero JavaScript unless you explicitly add interactive islands.
  • Islands architecture: interactive components are "islands" in a sea of static HTML. Use client:* directives to hydrate specific components.
  • Astro components (.astro): for static, non-interactive UI. The frontmatter script runs at build time only.
  • Framework components: use React, Vue, or Svelte components for interactive parts. Mix frameworks on the same page.
  • Content collections: type-safe Markdown/MDX/JSON/YAML content with Zod schemas. Defined in src/content.config.ts.
  • File-based routing: files in src/pages/ become routes. .astro, .md, .mdx, and .ts/.js (API routes) are supported.
  • No client-side router by default: each page is a full HTML document. Add view transitions for SPA-like navigation without a JS router.

Essential Patterns

Astro Config (astro.config.mjs)

import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import vue from "@astrojs/vue";
import svelte from "@astrojs/svelte";
import mdx from "@astrojs/mdx";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  site: "https://example.com",

  integrations: [
    react(),
    vue(),
    svelte(),
    mdx(),
  ],

  vite: {
    plugins: [tailwindcss()],
  },

  // Output mode: "static" (default) or "server" (SSR)
  output: "static",

  // Markdown configuration
  markdown: {
    shikiConfig: {
      theme: "github-dark",
    },
  },
});

Tailwind CSS Entry (src/styles/global.css)

@import "tailwindcss";

Base Layout (src/layouts/BaseLayout.astro)

---
import { ViewTransitions } from "astro:transitions";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import "../styles/global.css";

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

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

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content={description} />
    <title>{title}</title>
    <ViewTransitions />
  </head>
  <body class="min-h-screen bg-gray-50">
    <Header />
    <main class="p-6">
      <slot />
    </main>
    <Footer />
  </body>
</html>

Astro Page

---
// src/pages/index.astro
import BaseLayout from "../layouts/BaseLayout.astro";
import Counter from "../components/react/Counter";
import SearchWidget from "../components/react/SearchWidget";

const features = [
  { title: "Fast", description: "Zero JS by default" },
  { title: "Flexible", description: "Use any UI framework" },
  { title: "Content-focused", description: "Built for content sites" },
];
---

<BaseLayout title="Home">
  <section class="mb-12 text-center">
    <h1 class="mb-4 text-4xl font-bold">Welcome to My Site</h1>
    <p class="text-lg text-gray-600">Built with Astro 5</p>
  </section>

  <section class="mb-12 grid grid-cols-3 gap-6">
    {features.map((feature) => (
      <div class="rounded border p-4">
        <h3 class="mb-2 font-semibold">{feature.title}</h3>
        <p class="text-sm text-gray-600">{feature.description}</p>
      </div>
    ))}
  </section>

  <!-- Interactive React island — hydrated on page load -->
  <Counter client:load initialCount={0} />

  <!-- Interactive React island — hydrated when visible in viewport -->
  <SearchWidget client:visible />
</BaseLayout>

Islands Architecture — Client Directives

---
import Counter from "../components/react/Counter";
import ContactForm from "../components/vue/ContactForm.vue";
import ThemeToggle from "../components/svelte/ThemeToggle.svelte";
---

<!-- client:load — hydrate immediately on page load -->
<!-- Use for: above-the-fold interactive elements -->
<Counter client:load />

<!-- client:visible — hydrate when the component scrolls into view -->
<!-- Use for: below-the-fold components, lazy widgets -->
<ContactForm client:visible />

<!-- client:idle — hydrate when the browser is idle -->
<!-- Use for: low-priority interactive elements -->
<ThemeToggle client:idle />

<!-- client:media — hydrate when a media query matches -->
<!-- Use for: mobile-only or desktop-only interactions -->
<MobileMenu client:media="(max-width: 768px)" />

<!-- client:only="react" — render ONLY on the client (no SSR) -->
<!-- Use for: components that depend on browser APIs at render time -->
<BrowserOnlyChart client:only="react" />

<!-- No directive — renders static HTML, zero JS shipped -->
<Counter />

React Island Component

// src/components/react/Counter.tsx
import { useState } from "react";

interface CounterProps {
  initialCount?: number;
}

export default function Counter({ initialCount = 0 }: CounterProps) {
  const [count, setCount] = useState(initialCount);

  return (
    <div className="flex items-center gap-4 rounded border p-4">
      <button
        onClick={() => setCount((c) => c - 1)}
        className="rounded bg-gray-200 px-3 py-1 hover:bg-gray-300"
      >
        -
      </button>
      <span className="text-xl font-bold">{count}</span>
      <button
        onClick={() => setCount((c) => c + 1)}
        className="rounded bg-blue-600 px-3 py-1 text-white hover:bg-blue-700"
      >
        +
      </button>
    </div>
  );
}

Content Collection Schema (src/content.config.ts)

import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

const authors = defineCollection({
  loader: glob({ pattern: "**/*.json", base: "./src/content/authors" }),
  schema: z.object({
    name: z.string(),
    bio: z.string(),
    avatar: z.string().optional(),
  }),
});

export const collections = { blog, authors };

Blog List Page (Querying Content Collections)

---
// src/pages/blog/index.astro
import BaseLayout from "../../layouts/BaseLayout.astro";
import { getCollection } from "astro:content";

const posts = (await getCollection("blog"))
  .filter((post) => !post.data.draft)
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

<BaseLayout title="Blog">
  <h1 class="mb-6 text-3xl font-bold">Blog</h1>

  <div class="space-y-4">
    {posts.map((post) => (
      <a href={`/blog/${post.id}`} class="block rounded border p-4 hover:bg-gray-100">
        <h2 class="text-xl font-semibold">{post.data.title}</h2>
        <p class="text-sm text-gray-500">
          {post.data.pubDate.toLocaleDateString("en-US", {
            year: "numeric",
            month: "long",
            day: "numeric",
          })}
        </p>
        <p class="mt-1 text-gray-600">{post.data.description}</p>
        <div class="mt-2 flex gap-2">
          {post.data.tags.map((tag) => (
            <span class="rounded bg-gray-200 px-2 py-0.5 text-xs">{tag}</span>
          ))}
        </div>
      </a>
    ))}
  </div>
</BaseLayout>

Dynamic Blog Post Page

---
// src/pages/blog/[...slug].astro
import BaseLayout from "../../layouts/BaseLayout.astro";
import { getCollection, render } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<BaseLayout title={post.data.title} description={post.data.description}>
  <article class="prose mx-auto max-w-3xl">
    <h1>{post.data.title}</h1>
    <time class="text-sm text-gray-500">
      {post.data.pubDate.toLocaleDateString("en-US", {
        year: "numeric",
        month: "long",
        day: "numeric",
      })}
    </time>
    {post.data.heroImage && (
      <img src={post.data.heroImage} alt={post.data.title} class="my-6 rounded" />
    )}
    <Content />
  </article>
</BaseLayout>

MDX Content Example

---
title: "Interactive Blog Post"
description: "A post with interactive React components"
pubDate: 2025-12-01
tags: ["astro", "react", "mdx"]
---

import Counter from "../../components/react/Counter";

# Interactive Blog Post

This is a regular Markdown paragraph.

Here is an interactive counter embedded in the post:

<Counter client:visible initialCount={5} />

And the content continues below with more Markdown.

View Transitions

---
// View transitions are enabled by adding <ViewTransitions /> to the <head>
// (already included in BaseLayout above)

// Customize transitions on individual elements:
---

<h1 transition:name="page-title" transition:animate="slide">
  Page Title
</h1>

<img
  src="/hero.jpg"
  alt="Hero"
  transition:name="hero-image"
  transition:animate="fade"
/>

<!-- Available built-in animations: fade, slide, morph, none -->
<!-- Elements with matching transition:name across pages animate between them -->

API Endpoint

// src/pages/api/health.ts
import type { APIRoute } from "astro";

export const GET: APIRoute = () => {
  return new Response(
    JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }),
    {
      status: 200,
      headers: { "Content-Type": "application/json" },
    }
  );
};

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();

  return new Response(JSON.stringify({ received: body }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
};

SSR Mode (Server Output)

// astro.config.mjs — switch to server rendering
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  output: "server",
  adapter: node({ mode: "standalone" }),
});

With SSR, you can use dynamic server-side logic:

---
// This runs on every request (not at build time)
const response = await fetch("https://api.example.com/data");
const data = await response.json();

// Access request info
const url = Astro.url;
const cookies = Astro.cookies;
const headers = Astro.request.headers;

// Redirect
if (!cookies.has("session")) {
  return Astro.redirect("/login", 302);
}
---

Per-Page Rendering Control (Hybrid Mode)

// astro.config.mjs
export default defineConfig({
  output: "static", // Default static
});
---
// Force this page to be server-rendered even in static mode
export const prerender = false;
---
---
// Force this page to be prerendered even in server mode
export const prerender = true;
---

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Install dependencies: npm install
  3. Start dev server: npm run dev
  4. Verify the app loads at http://localhost:4321
  5. Run npx astro check to confirm diagnostics pass

Common Commands

# Development
npm run dev                    # Start dev server (http://localhost:4321)

# Build
npm run build                  # Production build (to dist/)
npm run preview                # Preview production build locally

# Add integrations
npx astro add react            # Add React support
npx astro add vue              # Add Vue support
npx astro add svelte           # Add Svelte support
npx astro add mdx              # Add MDX support
npx astro add tailwind         # Add Tailwind CSS

# Check
npx astro check                # Run Astro diagnostics + type checking

# Info
npx astro info                 # Show environment and config info

Integration Notes

  • Content/CMS: Astro's content collections are built-in. For headless CMS integration, fetch from Contentful, Sanity, Strapi, or any API in the frontmatter.
  • Testing: use Vitest for unit testing utility functions and components. Playwright for E2E testing the built site.
  • Deployment: adapters available for Vercel (@astrojs/vercel), Netlify (@astrojs/netlify), Cloudflare (@astrojs/cloudflare), Node (@astrojs/node), and Deno (@astrojs/deno). Static output deploys anywhere (S3, GitHub Pages, etc.).
  • Styling: Tailwind CSS is the recommended approach. Astro also supports scoped <style> tags in .astro files, CSS modules, and Sass.
  • Images: use Astro's built-in <Image /> component from astro:assets for automatic optimization, resizing, and format conversion.
  • SEO: no client-side router means every page is fully server-rendered HTML. Metadata is straightforward in the <head>. Add astro-seo integration for structured data.
  • Auth: in SSR mode, use server-side cookies/sessions. In static mode, handle auth entirely client-side in island components.
  • RSS: use @astrojs/rss package to generate RSS feeds from content collections.
  • Sitemap: add @astrojs/sitemap integration for automatic sitemap generation.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.