agentleFS
theyoungastronautstheyoungastronauts/polaris/skills/astro-bootstrap/SKILL.md

astro-bootstrap

Skill4 starsChanged 3 months agoMITView on GitHub

Scaffold an Astro project (Tailwind v4, DaisyUI, landing page).

  • Reads credentials
  • Installs packages

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 3 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Ask the user for these values:

    line 17
  2. Distinctive:

    If Agentation enabled: Import and add the React integration:

    line 124
  3. Very specific:

    If Agentation enabled: Add the import to the frontmatter and the overlay inside <body>, after <Footer />:

    line 242
  4. Distinctive:

    Only generate these files if the user opted into Agentation.

    line 539
  5. Very specific:

    Verify Docker files exist: Dockerfile.dev, docker-compose.yml, Makefile

    line 693
  6. Very specific:

    Verify page loads at http://localhost:{host_port}

    line 696
  7. Very specific:

    If content collections: verify blog index renders at /blog

    line 698
  8. Very specific:

    If Agentation: verify overlay renders in dev mode, check .mcp.json exists

    line 699

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Astro Bootstrap

On-demand command for scaffolding a new Astro site with Docker, Tailwind CSS v4, DaisyUI, and landing page conventions.

MANDATORY: This project runs entirely in Docker. Every Docker file below (Dockerfile.dev, docker-compose.yml, Makefile) MUST be generated. The Makefile is the sole interface — never run npm directly on the host.

Before You Start

Ask the user for these values:

PlaceholderDescriptionExample
{site-name}Project/directory nameacme-landing
{compose-project}Docker Compose project name (kebab-case)acme-landing
{host_port}Host port for Astro (default: 4321)4321
{domain}Production domainacme.com
{site-title}Display name for metadataAcme Inc
{site-description}Default meta descriptionBuild better widgets with Acme
Content collectionsInclude content collections setup?Yes / No
Include Agentation?Browser annotation tool for visual feedbackYes / No

Directory Structure

{site-name}/
├── src/
│   ├── pages/
│   │   ├── index.astro
│   │   └── 404.astro
│   ├── components/
│   │   ├── layout/
│   │   │   ├── Header.astro
│   │   │   └── Footer.astro
│   │   ├── landing/
│   │   │   └── Hero.astro
│   │   ├── ui/
│   │   │   └── Button.astro
│   │   └── dev/                # If Agentation enabled
│   │       └── AgentationOverlay.tsx
│   ├── layouts/
│   │   └── BaseLayout.astro
│   ├── styles/
│   │   └── global.css
│   ├── content/              # If content collections enabled
│   │   └── blog/
│   │       └── first-post.md
│   └── content.config.ts     # If content collections enabled
├── public/
│   ├── favicon.svg
│   └── robots.txt
├── package.json
├── astro.config.mjs
├── tsconfig.json
├── .env
├── .gitignore
├── Dockerfile.dev
├── docker-compose.yml
├── Makefile
└── .mcp.json                 # If Agentation enabled

File Templates

package.json

{
  "name": "{site-name}",
  "type": "module",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "astro dev --host",
    "build": "astro build",
    "preview": "astro preview --host",
    "check": "astro check"
  },
  "dependencies": {
    "astro": "^5",
    "@astrojs/sitemap": "^3"
  },
  "devDependencies": {
    "typescript": "^5",
    "tailwindcss": "^4",
    "@tailwindcss/vite": "^4",
    "daisyui": "^5"
  }
}

If content collections enabled: Also add "zod": "^3" to dependencies.

If Agentation enabled: Also add "@astrojs/react": "^4", "react": "^19", "react-dom": "^19" to dependencies and "agentation": "^0" to devDependencies.

astro.config.mjs

import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://{domain}',
  output: 'static',
  integrations: [sitemap()],
  vite: {
    plugins: [tailwindcss()],
  },
});

If Agentation enabled: Import and add the React integration:

import react from '@astrojs/react';

// Add to integrations array:
integrations: [sitemap(), react()],

tsconfig.json

{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

.env

PUBLIC_SITE_URL=https://{domain}

.gitignore

# astro
dist/
.astro/

# dependencies
node_modules/

# environment
.env*.local

# editor
.DS_Store
*.swp

# docker
docker-compose.override.yml

src/styles/global.css

@import "tailwindcss";
@plugin "daisyui" {
  themes: light, dark;
}

src/layouts/BaseLayout.astro

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

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

const {
  title,
  description = '{site-description}',
  ogImage = '/og.jpg',
} = Astro.props;

const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---

<!doctype html>
<html lang="en" data-theme="dark">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />

    <title>{title} | {site-title}</title>
    <meta name="description" content={description} />
    <link rel="canonical" href={canonicalURL} />

    <meta property="og:type" content="website" />
    <meta property="og:url" content={canonicalURL} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={new URL(ogImage, Astro.site)} />
    <meta property="og:site_name" content="{site-title}" />

    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content={title} />
    <meta name="twitter:description" content={description} />
    <meta name="twitter:image" content={new URL(ogImage, Astro.site)} />

    <ViewTransitions />
  </head>
  <body class="min-h-screen bg-base-100 text-base-content flex flex-col">
    <Header />
    <main class="flex-1">
      <slot />
    </main>
    <Footer />
  </body>
</html>

If Agentation enabled: Add the import to the frontmatter and the overlay inside <body>, after <Footer />:

---
import AgentationOverlay from '@/components/dev/AgentationOverlay.tsx';
---

<!-- Add after <Footer />, before </body>: -->
{import.meta.env.DEV && <AgentationOverlay client:only="react" />}

src/components/layout/Header.astro

---
const navItems = [
  { label: 'Home', href: '/' },
  { label: 'About', href: '/about' },
];
---

<header class="navbar bg-base-200 border-b border-base-300">
  <div class="container mx-auto max-w-5xl">
    <div class="flex-1">
      <a href="/" class="text-xl font-bold text-primary">{site-title}</a>
    </div>
    <nav class="flex-none">
      <ul class="menu menu-horizontal px-1">
        {navItems.map((item) => (
          <li><a href={item.href}>{item.label}</a></li>
        ))}
      </ul>
    </nav>
  </div>
</header>

src/components/layout/Footer.astro

---
const year = new Date().getFullYear();
---

<footer class="footer footer-center p-6 bg-base-200 text-base-content/70 border-t border-base-300">
  <p>&copy; {year} {site-title}. All rights reserved.</p>
</footer>

src/components/landing/Hero.astro

---
interface Props {
  title: string;
  subtitle?: string;
  ctaText?: string;
  ctaHref?: string;
}

const {
  title,
  subtitle,
  ctaText = 'Get Started',
  ctaHref = '#features',
} = Astro.props;
---

<section class="hero min-h-[60vh] bg-base-200">
  <div class="hero-content text-center">
    <div class="max-w-2xl">
      <h1 class="text-5xl font-bold">{title}</h1>
      {subtitle && <p class="py-6 text-lg text-base-content/70">{subtitle}</p>}
      <a href={ctaHref} class="btn btn-primary btn-lg">{ctaText}</a>
    </div>
  </div>
</section>

src/components/ui/Button.astro

---
interface Props {
  variant?: 'primary' | 'ghost' | 'outline';
  size?: 'sm' | 'md' | 'lg';
  href?: string;
}

const { variant = 'primary', size = 'md', href } = Astro.props;
const Tag = href ? 'a' : 'button';
const classes = `btn btn-${variant} btn-${size}`;
---

<Tag href={href} class={classes}>
  <slot />
</Tag>

src/pages/index.astro

---
import BaseLayout from '@/layouts/BaseLayout.astro';
import Hero from '@/components/landing/Hero.astro';
---

<BaseLayout title="Home" description="{site-description}">
  <Hero
    title="Welcome to {site-title}"
    subtitle="{site-description}"
    ctaText="Get Started"
    ctaHref="#features"
  />

  <section id="features" class="container mx-auto max-w-5xl px-4 py-16">
    <h2 class="text-3xl font-bold text-center mb-12">Features</h2>
    <div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
      <div class="card bg-base-200 shadow-md">
        <div class="card-body">
          <h3 class="card-title">Feature One</h3>
          <p class="text-base-content/70">Description of the first feature.</p>
        </div>
      </div>
      <div class="card bg-base-200 shadow-md">
        <div class="card-body">
          <h3 class="card-title">Feature Two</h3>
          <p class="text-base-content/70">Description of the second feature.</p>
        </div>
      </div>
      <div class="card bg-base-200 shadow-md">
        <div class="card-body">
          <h3 class="card-title">Feature Three</h3>
          <p class="text-base-content/70">Description of the third feature.</p>
        </div>
      </div>
    </div>
  </section>

  <section class="bg-base-200 py-16">
    <div class="container mx-auto max-w-5xl px-4 text-center">
      <h2 class="text-3xl font-bold mb-4">Ready to get started?</h2>
      <p class="text-base-content/70 mb-8 max-w-lg mx-auto">
        Join thousands of users building with {site-title}.
      </p>
      <a href="/signup" class="btn btn-primary btn-lg">Sign Up Free</a>
    </div>
  </section>
</BaseLayout>

src/pages/404.astro

---
import BaseLayout from '@/layouts/BaseLayout.astro';
---

<BaseLayout title="Page Not Found">
  <div class="min-h-[60vh] flex flex-col items-center justify-center px-4">
    <h1 class="text-6xl font-bold text-base-content/20 mb-4">404</h1>
    <p class="text-xl mb-8">Page not found.</p>
    <a href="/" class="btn btn-primary">Back to Home</a>
  </div>
</BaseLayout>

public/robots.txt

User-agent: *
Allow: /

Sitemap: https://{domain}/sitemap-index.xml

public/favicon.svg

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none">
  <rect width="36" height="36" rx="8" fill="#570DF8"/>
  <text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-size="20" fill="white" font-family="system-ui">A</text>
</svg>

Content Collections (if enabled)

src/content.config.ts

import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

src/content/blog/first-post.md

---
title: "First Post"
description: "Welcome to the blog."
pubDate: 2025-01-01
---

This is the first blog post. Edit or delete this file to get started.

src/pages/blog/index.astro

---
import { getCollection } from 'astro:content';
import BaseLayout from '@/layouts/BaseLayout.astro';

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

<BaseLayout title="Blog" description="Latest posts from {site-title}">
  <div class="container mx-auto max-w-3xl px-4 py-16">
    <h1 class="text-3xl font-bold mb-8">Blog</h1>
    {posts.length === 0 ? (
      <p class="text-base-content/70">No posts yet.</p>
    ) : (
      <ul class="space-y-6">
        {posts.map((post) => (
          <li>
            <a href={`/blog/${post.id}`} class="card bg-base-200 shadow-md hover:shadow-lg transition-shadow">
              <div class="card-body">
                <h2 class="card-title">{post.data.title}</h2>
                <p class="text-base-content/70 text-sm">{post.data.description}</p>
                <time class="text-xs text-base-content/50">
                  {post.data.pubDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
                </time>
              </div>
            </a>
          </li>
        ))}
      </ul>
    )}
  </div>
</BaseLayout>

src/pages/blog/[id].astro

---
import { getCollection, render } from 'astro:content';
import BaseLayout from '@/layouts/BaseLayout.astro';

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

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

<BaseLayout title={post.data.title} description={post.data.description}>
  <article class="container mx-auto max-w-3xl px-4 py-16">
    <header class="mb-8">
      <h1 class="text-4xl font-bold mb-2">{post.data.title}</h1>
      <time class="text-sm text-base-content/50">
        {post.data.pubDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
      </time>
    </header>
    <div class="prose prose-lg max-w-none">
      <Content />
    </div>
  </article>
</BaseLayout>

Agentation (if enabled)

Only generate these files if the user opted into Agentation.

src/components/dev/AgentationOverlay.tsx

import { Agentation } from "agentation";

export default function AgentationOverlay() {
  return <Agentation />;
}

.mcp.json

{
  "mcpServers": {
    "agentation": {
      "command": "npx",
      "args": ["agentation-mcp", "server"]
    }
  }
}

Docker (REQUIRED — do not skip)

Dockerfile.dev

FROM node:22-alpine

WORKDIR /app

# Install dependencies first for layer caching
COPY package.json package-lock.json* ./
RUN npm install

# Copy application code
COPY . .

EXPOSE 4321

CMD ["npm", "run", "dev"]

docker-compose.yml

name: {compose-project}

services:
  site:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "{host_port}:4321"
    env_file:
      - .env

Makefile

# {compose-project} - Makefile
PROJECT_NAME = {compose-project}
DOCKER_COMPOSE = docker compose --project-name $(PROJECT_NAME)

# ============================================================================
# Docker
# ============================================================================

.PHONY: build up down restart stop ps logs bash wipe

build:
	$(DOCKER_COMPOSE) build

up:
	$(DOCKER_COMPOSE) up --detach

down:
	$(DOCKER_COMPOSE) down

restart: down up

stop:
	$(DOCKER_COMPOSE) stop

ps:
	$(DOCKER_COMPOSE) ps

logs:
	$(DOCKER_COMPOSE) logs -f site

bash:
	$(DOCKER_COMPOSE) exec site sh

wipe:
	@echo "WARNING: This will delete all containers and volumes!"
	@read -p "Are you sure? [y/N] " confirm && [ "$$confirm" = "y" ]
	$(DOCKER_COMPOSE) down -v --remove-orphans

# ============================================================================
# Development
# ============================================================================

.PHONY: dev preview astro-check

dev: up logs

preview:
	$(DOCKER_COMPOSE) exec site npm run preview

astro-check:
	$(DOCKER_COMPOSE) exec site npm run check

# ============================================================================
# Dependencies
# ============================================================================

.PHONY: install add

install:
	$(DOCKER_COMPOSE) exec site npm install

add:
	$(DOCKER_COMPOSE) exec site npm install $(filter-out $@,$(MAKECMDGOALS))

# ============================================================================
# Build
# ============================================================================

.PHONY: build-site

build-site:
	$(DOCKER_COMPOSE) exec site npm run build

# Catch-all for npm commands
%:
	@:

Post-Bootstrap Checklist

  1. Create project directory
  2. Generate all files from templates above (replace placeholders)
  3. Verify Docker files exist: Dockerfile.dev, docker-compose.yml, Makefile
  4. Copy .env from template above
  5. make build && make up
  6. Verify page loads at http://localhost:{host_port}
  7. make astro-check — should pass cleanly
  8. If content collections: verify blog index renders at /blog
  9. If Agentation: verify overlay renders in dev mode, check .mcp.json exists
  10. Commit initial scaffold

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.