frontend/astro-project-starter/SKILL.md
Scaffold an Astro 5.x project with content collections, islands architecture, framework integrations (React/Vue/Svelte), MDX, view transitions, and multi-adapter deployment.
npx skillsauth add achreftlili/deep-dev-skills astro-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold 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 v4
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
client:* 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.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.