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 repositoryBest 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": []
}
```