Scaffold an Astro 5.x project with content collections, islands architecture, framework integrations (React/Vue/Svelte), MDX, view transitions, and multi-adapter deployment.
84
80%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./frontend/astro-project-starter/SKILL.mdScaffold an Astro 5.x project with content collections, islands architecture, framework integrations (React/Vue/Svelte), MDX, view transitions, and multi-adapter deployment.
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 v4src/
├── 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 templateclient:* directives to hydrate specific components..astro): for static, non-interactive UI. The frontmatter script runs at build time only.src/content.config.ts.src/pages/ become routes. .astro, .md, .mdx, and .ts/.js (API routes) are supported.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",
},
},
});src/styles/global.css)@import "tailwindcss";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>---
// 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>---
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 />// 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>
);
}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 };---
// 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>---
// 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>---
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 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 -->// 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" },
});
};// 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);
}
---// 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;
---.env.example to .env and fill in valuesnpm installnpm run devnpx astro check to confirm diagnostics pass# 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@astrojs/vercel), Netlify (@astrojs/netlify), Cloudflare (@astrojs/cloudflare), Node (@astrojs/node), and Deno (@astrojs/deno). Static output deploys anywhere (S3, GitHub Pages, etc.).<style> tags in .astro files, CSS modules, and Sass.<Image /> component from astro:assets for automatic optimization, resizing, and format conversion.<head>. Add astro-seo integration for structured data.@astrojs/rss package to generate RSS feeds from content collections.@astrojs/sitemap integration for automatic sitemap generation.181fcbc
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.