Back to skills
SkillHub ClubShip Full StackFull Stack

astro

Imported from https://github.com/openclaw/skills.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,110
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
F28.0

Install command

npx @skill-hub/cli install openclaw-skills-astro

Repository

openclaw/skills

Skill path: skills/bezkom/astro

Imported from https://github.com/openclaw/skills.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install astro into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding astro to shared team environments
  • Use astro for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: astro
description: Deploy multilingual static websites for free on Cloudflare using Astro framework with markdown source files. Use when: (1) Creating new static sites or blogs, (2) Setting up multilingual (i18n) content, (3) Deploying to Cloudflare Pages, (4) Converting markdown to static websites, (5) Setting up free hosting infrastructure.
---

# Astro Static Site Generator

Deploy multilingual static websites for free on Cloudflare using Astro framework.

## Prerequisites

- Node.js 20+ installed
- Cloudflare account (free)
- Git repository (GitHub, GitLab, or Bitbucket)

## Quick Start

### 1. Create Project

```bash
npm create astro@latest my-site -- --template minimal
cd my-site
npm install
```

### 2. Configure for Cloudflare

**Static Sites (Recommended for most use cases)**

No adapter needed. Use default static output:

```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://your-site.pages.dev',
});
```

**SSR/Edge Functions (Optional)**

If you need server-side rendering or edge functions:

```bash
npm install @astrojs/cloudflare
```

```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  output: 'server',
  adapter: cloudflare(),
  site: 'https://your-site.pages.dev',
});
```

### 3. Deploy to Cloudflare

**Git Integration (Recommended)**

1. Push to GitHub/GitLab
2. Cloudflare Dashboard → Pages → Create project → Connect to Git
3. Configure:
   - Build command: `npm run build`
   - Build output: `dist`

**Direct Upload**

```bash
# Deploy (authenticate via Cloudflare dashboard or wrangler)
npx wrangler pages deploy dist
```

## Multilingual Configuration

### Astro Config

```javascript
// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr', 'de'],
    routing: {
      prefixDefaultLocale: false,  // /about instead of /en/about
    },
  },
});
```

**Routing Modes:**

| Setting | URL Structure | Best For |
|---------|--------------|----------|
| `prefixDefaultLocale: false` | `/about`, `/es/about` | Default locale at root |
| `prefixDefaultLocale: true` | `/en/about`, `/es/about` | All locales prefixed |

### Content Structure

```
src/content/
├── config.ts          # Content collection schema
└── docs/
    ├── en/
    │   ├── index.md
    │   └── guide.md
    ├── es/
    │   ├── index.md
    │   └── guide.md
    └── fr/
        ├── index.md
        └── guide.md
```

### Content Collection Schema

```typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const docs = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    lang: z.enum(['en', 'es', 'fr', 'de']),
  }),
});

export const collections = { docs };
```

**Note:** Run `npx astro sync` after adding content collections to generate types.

### Language Switcher Component

```astro
---
// src/components/LanguageSwitcher.astro
const languages = {
  en: 'English',
  es: 'Español',
  fr: 'Français',
  de: 'Deutsch',
};

const currentPath = Astro.url.pathname;
const currentLang = Astro.currentLocale || 'en';
---

<select onchange="window.location = this.value">
  {Object.entries(languages).map(([code, name]) => (
    <option 
      value={`/${code}${currentPath}`} 
      selected={code === currentLang}
    >
      {name}
    </option>
  ))}
</select>
```

## File Structure

```
my-site/
├── astro.config.mjs      # Astro configuration
├── package.json
├── public/
│   ├── favicon.svg
│   └── _redirects        # Cloudflare redirects (optional)
├── src/
│   ├── components/
│   │   └── LanguageSwitcher.astro
│   ├── content/
│   │   ├── config.ts
│   │   └── blog/
│   │       ├── en/
│   │       └── es/
│   ├── layouts/
│   │   └── BaseLayout.astro
│   └── pages/
│       ├── index.astro
│       ├── en/
│       │   └── index.astro
│       └── es/
│           └── index.astro
```

## Cloudflare Pages Settings

| Setting | Value |
|---------|-------|
| Build command | `npm run build` |
| Build output | `dist` |
| Node version | `20` |
| Environment | `NODE_VERSION=20` |

### Custom Domain

Cloudflare Dashboard → Pages → your-site → Custom domains → Add domain

### Redirects

Create `public/_redirects`:

```
/  /en/  302
/old-page  /new-page  301
```

## Commands Reference

| Command | Description |
|---------|-------------|
| `npm run dev` | Start dev server |
| `npm run build` | Build for production |
| `npm run preview` | Preview production build |
| `npx astro sync` | Generate content collection types |
| `npx wrangler login` | Authenticate with Cloudflare |
| `npx wrangler pages deploy dist` | Deploy to Cloudflare |

## Blog with Content Collections

```astro
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';

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

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

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>
```

## Troubleshooting

### Build Fails on Cloudflare

Set `NODE_VERSION=20` in Cloudflare Pages environment variables.

### 404 on Nested Routes

```javascript
// astro.config.mjs
export default defineConfig({
  trailingSlash: 'always',
});
```

### i18n Not Working

Ensure:
1. Locales match folder names exactly
2. Content files have correct `lang` frontmatter
3. Run `npx astro sync` after creating content collections

### Content Collection Type Errors

Run `npx astro sync` to generate TypeScript types.

## Resources

- [Astro Docs](https://docs.astro.build)
- [Cloudflare Pages Docs](https://developers.cloudflare.com/pages)
- [Astro i18n Guide](https://docs.astro.build/en/guides/i18n/)
- [Cloudflare Adapter](https://docs.astro.build/en/guides/deploy/cloudflare/)

## Scripts

| Script | Description |
|--------|-------------|
| `astro-new-post.py` | Create multilingual blog posts |
| `astro-i18n-check.py` | Validate translation coverage |

### Script Usage

```bash
# Create a new post in multiple languages
python scripts/astro-new-post.py --title "My Post" --langs en,es,fr

# Create with author and tags
python scripts/astro-new-post.py --title "Tutorial" --langs en,es --author "John" --tags tutorial,astro

# Check translation coverage
python scripts/astro-i18n-check.py --langs en,es,fr

# Check specific content directory
python scripts/astro-i18n-check.py --content-dir src/content/blog --langs en,es

# Output as JSON
python scripts/astro-i18n-check.py --langs en,es,fr --json
```

All scripts use only Python standard library (no dependencies).


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/astro-new-post.py

```python
#!/usr/bin/env python3
"""
Create a new multilingual blog post for Astro.

Usage:
    python astro-new-post.py --title "My Post" --langs en,es,fr
    python astro-new-post.py --title "My Post" --langs en,es,fr --dir src/content/blog
"""

import argparse
import os
from datetime import datetime
from pathlib import Path


def create_post(title: str, lang: str, content_dir: Path, author: str = "", tags: list = None) -> Path:
    """Create a markdown file for a specific language."""
    
    # Create language directory
    lang_dir = content_dir / lang
    lang_dir.mkdir(parents=True, exist_ok=True)
    
    # Generate slug from title
    slug = title.lower().replace(" ", "-").replace("'", "")
    for char in [",", ".", "!", "?", ":", ";"]:
        slug = slug.replace(char, "")
    
    # Create filename
    filename = f"{slug}.md"
    filepath = lang_dir / filename
    
    # Check if file already exists
    if filepath.exists():
        print(f"⚠️  File already exists: {filepath}")
        return filepath
    
    # Generate frontmatter
    today = datetime.now().strftime("%Y-%m-%d")
    tags_str = str(tags) if tags else "[]"
    
    frontmatter = f"""---
title: "{title}"
description: ""
pubDate: {today}
author: "{author}"
lang: "{lang}"
tags: {tags_str}
---

# {title}

Write your content here...
"""
    
    # Write file
    filepath.write_text(frontmatter, encoding="utf-8")
    print(f"✅ Created: {filepath}")
    
    return filepath


def main():
    parser = argparse.ArgumentParser(
        description="Create a new multilingual blog post for Astro"
    )
    parser.add_argument(
        "--title", "-t",
        required=True,
        help="Post title"
    )
    parser.add_argument(
        "--langs", "-l",
        default="en",
        help="Comma-separated language codes (e.g., en,es,fr)"
    )
    parser.add_argument(
        "--dir", "-d",
        default="src/content/blog",
        help="Content directory path (default: src/content/blog)"
    )
    parser.add_argument(
        "--author", "-a",
        default="",
        help="Author name"
    )
    parser.add_argument(
        "--tags",
        default="",
        help="Comma-separated tags (e.g., tutorial,astro)"
    )
    
    args = parser.parse_args()
    
    # Parse languages
    langs = [l.strip() for l in args.langs.split(",")]
    
    # Parse tags
    tags = [t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else []
    
    # Resolve content directory
    content_dir = Path(args.dir)
    
    print(f"📝 Creating post: '{args.title}'")
    print(f"🌍 Languages: {', '.join(langs)}")
    print()
    
    # Create post for each language
    for lang in langs:
        create_post(args.title, lang, content_dir, args.author, tags)
    
    print()
    print("🎉 Done! Edit the files to add your content.")


if __name__ == "__main__":
    main()

```

### scripts/astro-i18n-check.py

```python
#!/usr/bin/env python3
"""
Validate translation completeness across all locales.

Checks that for each content file in the default language,
corresponding files exist in all other locales.

Usage:
    python astro-i18n-check.py
    python astro-i18n-check.py --content-dir src/content/docs
    python astro-i18n-check.py --default-lang en --langs en,es,fr
"""

import argparse
import os
from pathlib import Path
from collections import defaultdict


def get_content_files(content_dir: Path, lang: str) -> set:
    """Get all markdown files for a specific language."""
    lang_dir = content_dir / lang
    if not lang_dir.exists():
        return set()
    
    files = set()
    for md_file in lang_dir.rglob("*.md"):
        # Get relative path within language directory
        relative = md_file.relative_to(lang_dir)
        files.add(str(relative))
    
    return files


def check_translations(content_dir: Path, default_lang: str, langs: list) -> dict:
    """Check translation coverage for all content."""
    
    results = {
        "missing": defaultdict(list),
        "extra": defaultdict(list),
        "complete": [],
        "stats": {}
    }
    
    # Get files in default language
    default_files = get_content_files(content_dir, default_lang)
    
    if not default_files:
        print(f"⚠️  No content found in default language '{default_lang}'")
        return results
    
    results["stats"]["total"] = len(default_files)
    results["stats"]["default_lang"] = default_lang
    results["stats"]["langs"] = langs
    
    # Check each other language
    for lang in langs:
        if lang == default_lang:
            continue
        
        lang_files = get_content_files(content_dir, lang)
        
        # Missing: in default but not in this lang
        missing = default_files - lang_files
        for f in sorted(missing):
            results["missing"][lang].append(f)
        
        # Extra: in this lang but not in default
        extra = lang_files - default_files
        for f in sorted(extra):
            results["extra"][lang].append(f)
        
        # Track complete files
        if not missing and not extra:
            results["complete"].append(lang)
    
    return results


def print_report(results: dict):
    """Print a formatted report."""
    
    stats = results["stats"]
    print("=" * 60)
    print("📊 Translation Coverage Report")
    print("=" * 60)
    print(f"Default language: {stats['default_lang']}")
    print(f"Total content files: {stats['total']}")
    print(f"Target languages: {', '.join(stats['langs'])}")
    print()
    
    # Missing translations
    if results["missing"]:
        print("❌ Missing Translations:")
        print("-" * 40)
        for lang, files in sorted(results["missing"].items()):
            print(f"\n{lang.upper()} ({len(files)} missing):")
            for f in files:
                print(f"  • {f}")
        print()
    
    # Extra translations
    if results["extra"]:
        print("⚠️  Extra Translations (not in default):")
        print("-" * 40)
        for lang, files in sorted(results["extra"].items()):
            print(f"\n{lang.upper()} ({len(files)} extra):")
            for f in files:
                print(f"  • {f}")
        print()
    
    # Summary
    total_missing = sum(len(files) for files in results["missing"].values())
    total_extra = sum(len(files) for files in results["extra"].values())
    
    if total_missing == 0 and total_extra == 0:
        print("✅ All translations are complete!")
    else:
        print("=" * 60)
        print(f"📈 Summary: {total_missing} missing, {total_extra} extra")
        print("=" * 60)


def main():
    parser = argparse.ArgumentParser(
        description="Validate translation completeness"
    )
    parser.add_argument(
        "--content-dir", "-c",
        default="src/content",
        help="Content directory path"
    )
    parser.add_argument(
        "--default-lang", "-d",
        default="en",
        help="Default language code"
    )
    parser.add_argument(
        "--langs", "-l",
        default="en",
        help="Comma-separated language codes"
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON"
    )
    
    args = parser.parse_args()
    
    # Parse languages
    langs = [l.strip() for l in args.langs.split(",")]
    content_dir = Path(args.content_dir)
    
    if not content_dir.exists():
        print(f"❌ Content directory not found: {content_dir}")
        return 1
    
    results = check_translations(content_dir, args.default_lang, langs)
    
    if args.json:
        import json
        # Convert defaultdicts to regular dicts for JSON
        output = {
            "missing": {k: v for k, v in results["missing"].items()},
            "extra": {k: v for k, v in results["extra"].items()},
            "complete": results["complete"],
            "stats": results["stats"]
        }
        print(json.dumps(output, indent=2))
    else:
        print_report(results)
    
    # Return exit code based on missing translations
    total_missing = sum(len(files) for files in results["missing"].values())
    return 1 if total_missing > 0 else 0


if __name__ == "__main__":
    exit(main())

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "bezkom",
  "slug": "astro",
  "displayName": "Astro",
  "latest": {
    "version": "1.0.1",
    "publishedAt": 1772165281245,
    "commit": "https://github.com/openclaw/skills/commit/6707162a80779835905fb99ab83db1beb8423428"
  },
  "history": []
}

```