算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
frontend-astro-architecture【设计与创意】Use this skill when the user says 'Astro', 'Astro architecture', 'Astro islands', 'Astro component', 'Astro page', 'Astro content collection', 'Astro SSG', 'Astro SSR', '.astro file'. This skill enforces: zero-JS-by-default component model, island architecture with client:* directives, file-based routing, type-safe content collections with Zod schema…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build content-focused websites with Astro's zero-JS-by-default approach. Use islands for interactive parts, content collections for type-safe markdown, and adapters for deployment flexibility.
Exact user phrases: "Astro", "Astro architecture", "Astro islands", "Astro component", "Astro page", "Astro content collection", "Astro SSG", "Astro SSR", ".astro file".
Before activating, verify:
No file output. Produces component patterns, island strategies, content collection schemas, and deployment config as text.
Component code: .astro file template. Island patterns: client directive usage. Content: Zod schema + query.
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Component code: 20 lines. Island examples: 10 lines each.
| Approach | Trade-off | When to Use |
|---|---|---|
| Pure .astro components | Zero JS, fully static | Content pages, headers, layout shells |
| Framework islands via client:* | JS for interactivity | Widgets, forms, interactive UI |
| MDX components | Markdown + component interop | Blogs with embedded interactive content |
| Server endpoints (.ts) | JSON API, no HTML rendering | API routes, form handlers |
| Middleware | Per-request processing | Auth, redirects, logging |
Is the site mostly static content (blog, docs, marketing)?
├── Yes -> output: 'static' (SSG)
└── No -> Are some routes dynamic?
├── Yes -> output: 'hybrid' (SSG + SSR per route)
└── No -> output: 'server' (full SSR)
Does the component need client-side interactivity?
├── No -> .astro component (zero JS)
└── Yes -> Which framework?
├── Already in project -> Use it
├── Need smallest runtime -> Preact or SolidJS
├── Need rich ecosystem -> React
└── Need template syntax -> Vue or Svelte
When does the user need this component?
├── Immediately visible and interactive -> client:load
├── Visible but not urgent -> client:idle
├── Below the fold -> client:visible
├── Only on certain screen sizes -> client:media
└── Browser-only, no SSR needed -> client:only
Where does content live?
├── Local markdown/MDX files -> Content Collections
├── Headless CMS (Sanity, Contentful) -> CMS SDK in frontmatter
├── Database (SQLite, PostgreSQL) -> Server endpoint + fetch
└── Git-based (MDX in repo) -> Content Collections + MDX integration
Is data needed at build time or request time?
├── Build time (static) -> fetch in frontmatter, getStaticPaths
├── Request time (SSR) -> fetch in frontmatter, no getStaticPaths
└── Client-side (after hydration) -> fetch in framework component useEffect/onMount
What type of asset?
├── Images -> astro:assets Image/Picture
├── Fonts -> @fontsource or local font files in public/
├── Third-party scripts -> Partytown or data-astro-script
└── Styles -> <style> tag (scoped) or <style is:global> or CSS imports
---
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
const { title } = Astro.props
---
<!doctype html>
<html lang="en">
<head><title>{title}</title></head>
<body>
<Header />
<slot />
<Footer />
</body>
</html>
---
import Layout from '../layouts/BlogLayout.astro'
import BlogCard from '../components/BlogCard.astro'
const response = await fetch('https://api.example.com/posts')
const posts = await response.json()
---
<Layout title="Blog">
<h1>Blog</h1>
<ul>
{posts.map(post => (
<BlogCard title={post.title} slug={post.slug} date={post.date} />
))}
</ul>
</Layout>
---
export async function getStaticPaths() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
---
<h1>{post.title}</h1>
<article>{post.content}</article>
---
// This page is server-rendered even with output: 'hybrid'
export const prerender = false
const session = Astro.locals.session
if (!session) return Astro.redirect('/login')
---
<h1>Dashboard</h1>
---
import InteractiveMap from '../components/InteractiveMap.tsx'
---
<!-- Hydrates when page is idle -->
<InteractiveMap client:idle latitude={51.5} longitude={-0.12} />
// src/pages/api/posts.ts
export async function GET({ params, request }) {
const posts = await db.query('SELECT * FROM posts')
return new Response(JSON.stringify(posts), {
headers: { 'Content-Type': 'application/json' },
})
}
// src/middleware.ts
import { defineMiddleware } from 'astro/middleware'
export const onRequest = defineMiddleware(async (context, next) => {
const start = Date.now()
const response = await next()
const elapsed = Date.now() - start
response.headers.set('X-Render-Time', String(elapsed))
return response
})
---
import { getCollection } from 'astro:content'
const posts = await getCollection('blog', ({ data }) => !data.draft)
const sorted = posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
const featured = sorted.slice(0, 3)
---
{featured.map(post => <a href={`/blog/${post.slug}`}>{post.data.title}</a>)}
Astro pages are rendered on the server (SSG or SSR) and ship zero JS by default. State management only applies to framework islands, which are isolated from each other.
Pass initial data from Astro frontmatter to framework islands as props. Islands manage their own internal state:
---
import TodoWidget from '../components/TodoWidget.tsx'
const initialTodos = await getCollection('todos')
---
<TodoWidget client:load initialTodos={initialTodos.map(t => ({ id: t.id, text: t.data.text }))} />
// TodoWidget.tsx
import { useState } from 'react'
export default function TodoWidget({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos)
return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>
}
---
import Sender from '../components/Sender.svelte'
import Receiver from '../components/Receiver.svelte'
---
<Sender client:load />
<Receiver client:load />
<!-- Sender.svelte -->
<script>
function send() {
window.dispatchEvent(new CustomEvent('app:message', { detail: 'hello' }))
}
</script>
<button on:click={send}>Send</button>
<!-- Receiver.svelte -->
<script>
let message = ''
function handleEvent(e) { message = e.detail }
</script>
<svelte:window on:app:message={handleEvent} />
<p>{message}</p>
Use your framework's state management within individual islands. React islands can use Zustand or Jotai. Vue islands can use Pinia. Islands of different frameworks cannot share the same store instance.
For page-level state that should survive navigation, use URL search params:
---
const search = Astro.url.searchParams.get('q') || ''
const results = await searchPosts(search)
---
<SearchResults query={search} results={results} client:load />
Content-only pages with no framework islands ship zero JavaScript. Astro strips all JS from .astro components at build time, leaving only HTML and CSS. This gives perfect Lighthouse scores by default.
Each island type (React, Vue, etc.) loads its own framework runtime on first hydration. The runtime is cached and reused for subsequent islands of the same type. A page with 5 React islands pays the React runtime cost once (~45KB gzipped).
In SSR mode, Astro streams HTML to the browser. Framework islands are rendered as streams are flushed. Use Astro.slots.render() for streaming slot content.
astro:assets provides:
loading="lazy"Building thousands of pages in SSG mode uses significant memory. Each page renders in isolation. For sites with 100K+ pages, use output: 'hybrid' with on-demand rendering for less-visited routes.
<style> tags generate unique class names per component, preventing style leaks<style is:global> applies globally without scoping@astrojs/tailwind for utility-first CSS with automatic purging<script> tags in .astro files are bundled and optimizeddata-astro-script for fine-grained script loading control@astrojs/partytown for third-party scripts (analytics, tracking) — moves them to a web workerastro build generates static HTML in dist/ for SSG modeastro build --analyze or rollup-plugin-visualizer to inspect bundle sizescompressHTML: true in astro.config.mjs to minify HTML outputscopedStyleStrategy: 'class' to reduce CSS output size// astro.config.mjs
import { defineConfig } from 'astro/config'
import react from '@astrojs/react'
import sitemap from '@astrojs/sitemap'
export default defineConfig({
site: 'https://example.com',
output: 'hybrid',
integrations: [react(), sitemap()],
build: {
format: 'file', // 'file' for /about.html, 'directory' for /about/index.html
assets: '_assets', // Custom asset prefix
inlineStylesheets: 'auto', // 'always' | 'auto' | 'never'
},
compressHTML: true,
scopedStyleStrategy: 'class', // 'class' | 'attribute'
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
},
})
manualChunks to split vendor code in SSR modeAstro automatically adds content hashes to asset filenames for cache busting. Configure via vite.build.assetsInlineLimit to control inlining threshold for small assets.
Files in public/ are copied to the build output as-is, with no processing. Use for:
robots.txtfavicon.icoCNAME for custom domains---
// Accessible in frontmatter
const apiKey = import.meta.env.API_KEY
const mode = import.meta.env.MODE // 'development' | 'production'
const isDev = import.meta.env.DEV
const isProd = import.meta.env.PROD
---
Prefix public env vars with PUBLIC_ (e.g., PUBLIC_API_URL). Private env vars are only available in server-side code (frontmatter, endpoints, middleware).
Astro components render to HTML strings. Use astro/virtual-modules/astro-server for testing:
// components/__tests__/Header.test.ts
import { expect, test } from 'vitest'
import { render } from 'astro/test-utils'
test('Header renders title', async () => {
const html = await render('./src/components/Header.astro', { props: { title: 'Hello' } })
expect(html).toContain('Hello')
})
// src/content/__tests__/schemas.test.ts
import { expect, test } from 'vitest'
import { z } from 'astro:content'
const PostSchema = z.object({
title: z.string().min(1),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
})
test('validates post frontmatter', () => {
const valid = PostSchema.parse({ title: 'Test', pubDate: '2024-01-01' })
expect(valid.title).toBe('Test')
expect(() => PostSchema.parse({ pubDate: '2024-01-01' })).toThrow()
})
Test framework components (React, Vue, Svelte) using their respective testing libraries (React Testing Library, Vue Test Utils, etc.). Islands are standard framework components — no special Astro testing needed.
// e2e/home.spec.ts
import { test, expect } from '@playwright/test'
test('homepage is static with zero JS', async ({ page }) => {
await page.goto('/')
const jsSize = await page.evaluate(() =>
document.querySelectorAll('script').length
)
expect(jsSize).toBe(0) // No islands on this page
})
test('island hydrates on scroll', async ({ page }) => {
await page.goto('/blog')
const widget = page.locator('[data-island="chart"]')
await expect(widget).toBeVisible()
// Verify no JS loaded yet (below fold)
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await expect(widget).toBeHydrated() // Custom matcher
})
// e2e/api.spec.ts
import { test, expect } from '@playwright/test'
test('API endpoint returns JSON', async ({ request }) => {
const response = await request.get('/api/posts')
expect(response.ok()).toBeTruthy()
expect(await response.json()).toBeInstanceOf(Array)
})
Use @playwright/test with expect(page).toHaveScreenshot() for visual comparison. Astro's deterministic SSG output makes it ideal for visual regression testing.
Page conversion:
Next.js: pages/[slug].tsx with getStaticProps
Astro: src/pages/[slug].astro with frontmatter fetch
// Next.js pattern
export async function getStaticProps({ params }) {
const post = await getPost(params.slug)
return { props: { post } }
}
---
// Astro equivalent
export async function getStaticPaths() {
const posts = await getPosts()
return posts.map(p => ({ params: { slug: p.slug }, props: { post: p } }))
}
const { post } = Astro.props
---
Layout conversion:
Next.js: layouts/PostLayout.tsx with {children}
Astro: layouts/PostLayout.astro with <slot />
Image conversion:
next/image -> astro:assets Image
getServerSideProps -> frontmatter fetch or client:only island
API routes -> src/pages/api/*.ts endpoints
Middleware -> src/middleware.ts
Gatsby projects map well to Astro since both are SSG-first:
gatsby-plugin-image -> astro:assets Image
gatsby-source-filesystem -> Content Collections
gatsby-node.js createPages -> getStaticPaths
GraphQL queries -> frontmatter fetch or Content Collection queries
Gatsby Link -> standard <a> tags or <ClientRouter>
Strategy 1: Incremental adoption via subdomain Migrate content pages first (blog, docs, marketing) while keeping the SPA for the app shell:
app.example.com -> SPA (unchanged)
www.example.com -> Astro (new)
Strategy 2: Full migration with island preservation Wrap existing SPA components as Astro islands:
---
import OldReactComponent from '../components/OldReactComponent.tsx'
---
<OldReactComponent client:load />
Strategy 3: Hybrid with iframe Embed the legacy SPA in an iframe for transitional periods, replacing sections incrementally.
WordPress/WP REST API -> fetch in frontmatter, generate static pages
MDX in Next.js -> Content Collections with MDX integration
Notion/Google Docs -> Export to markdown, import to src/content/
Sanity/Contentful -> CMS SDK calls in frontmatter
Using client:load on every island when client:visible or client:idle would suffice. Every hydrated island downloads its framework runtime eagerly. Audit hydration directives regularly.
Attempting to share a React context or Vue provide/inject across different islands. Islands are independent React/Vue/Svelte roots — they cannot share reactive state directly. Use DOM events, URL state, or localStorage instead.
---
// Anti-pattern: component fetching its own data
const item = await fetch(`https://api.example.com/items/${Astro.props.id}`).then(r => r.json())
---
Fetch data in page frontmatter and pass as props. Component frontmatter re-runs in every context, making it harder to cache and control.
In SSG mode, dynamic routes like [slug].astro require getStaticPaths to define all possible paths at build time. Without it, the build will fail for dynamic segments.
Using React, Vue, and Svelte islands on the same page downloads three separate framework runtimes (~63KB combined). Stick to one framework for all islands when possible.
Setting output: 'server' or output: 'hybrid' without configuring an adapter causes build errors. SSR/hybrid modes require @astrojs/node, @astrojs/vercel, or similar.
client:only skips SSR entirely. If the component relies on initial HTML from the server (SEO-critical content, meta tags), use client:load instead.
Putting an entire page inside a framework island defeats Astro's purpose. Only island the interactive parts — keep the static shell as .astro components.
---
// Anti-pattern: frontmatter runs on server, no DOM access
document.title = 'My Page' // Will crash in SSG/SSR
---
Hydrating an island after paint can cause layout shift if no explicit dimensions are set on the container:
<!-- Anti-pattern -->
<Chart client:visible />
<!-- Correct -->
<Chart client:visible style="width: 100%; height: 400px;" />
Astro's power is zero-JS output. If a component doesn't need interactivity (click handlers, state, effects), keep it as a pure .astro component. Don't add a framework just for convenience.
// Wrong — missing adapter for SSR
export default defineConfig({ output: 'server' })
// Correct
import node from '@astrojs/node'
export default defineConfig({ output: 'server', adapter: node() })
SSR and hybrid modes require an adapter. SSG mode does not.
---
// Wrong — fetching in component, not page
---
Fetch data in page components and pass down via props. Component frontmatter runs on every render — keep it pure.
Without getStaticPaths, dynamic routes like [slug].astro cannot be pre-rendered in SSG mode. Always export it for dynamic SSG routes, or set prerender = false for SSR.
client:only skips SSR entirely. If the component relies on initial HTML from the server (SEO-critical content), use client:load instead. Use client:only only for components that need browser APIs at mount.
| Aspect | Astro | Next.js |
|---|---|---|
| Default output | Zero JS | JS by default |
| Hydration model | Island architecture | Server Components + Client Components |
| Content collections | Built-in with Zod | MDX, external CMS |
| Image optimization | Built-in (astro:assets) | Built-in (next/image) |
| Data fetching | In frontmatter (SSR/SSG) | Server Components, getServerSideProps |
| Bundle size | Proportional to islands | Proportional to page |
| Best for | Content sites, marketing | Full-stack apps, e-commerce |
Traditional SSGs lack Astro's island architecture and multi-framework support. Astro adds interactive widgets without sacrificing static performance.
Both minimize JS, but differently: Astro ships zero JS by default and hydrates entire framework islands on interaction. Qwik ships zero JS by default and lazy-loads individual event handlers. Astro is simpler for content sites; Qwik is more granular for complex interactivity.
| Package | Purpose |
|---|---|
| astro | Core framework |
| @astrojs/mdx | MDX support for content |
| @astrojs/tailwind | Tailwind integration |
| @astrojs/sitemap | Automatic sitemap generation |
| @astrojs/partytown | Third-party script optimization |
| @astrojs/markdoc | Markdoc support |
| Package | Framework |
|---|---|
| @astrojs/react | React 18+ |
| @astrojs/vue | Vue 3 |
| @astrojs/svelte | Svelte 4/5 |
| @astrojs/solid-js | SolidJS |
| @astrojs/preact | Preact |
| @astrojs/lit | Lit |
| @astrojs/alpinejs | Alpine.js |
.astro files, content collection schema validation, IntelliSense.npx astro add react).---
const { title, children } = Astro.props
const items = await fetch('https://api.example.com/items').then(r => r.json())
---
<h1>{title}</h1>
<ul>
{items.map(item => <li>{item.name}</li>)}
</ul>
{children}
<style is:global>h1 { color: slate; }</style>
---
import ReactCounter from '../components/ReactCounter.tsx'
import VueSlider from '../components/VueSlider.vue'
---
<ReactCounter client:load />
<VueSlider client:idle />
<ReactModal client:visible />
<Chart client:media="(min-width: 768px)" />
<Dashboard client:only="react" />
src/pages/
index.astro -> /
about.astro -> /about
blog/
index.astro -> /blog
[slug].astro -> /blog/:slug
tags/[tag].astro -> /blog/tags/:tag
Dynamic routes: export async function getStaticPaths() for SSG. Redirects in astro.config.mjs. Middleware in src/middleware.ts for SSR.
// src/content/config.ts
import { z, defineCollection } from 'astro:content'
const blog = defineCollection({
type: 'content',
schema: z.object({ title: z.string(), pubDate: z.coerce.date(), tags: z.array(z.string()).default([]) }),
})
export const collections = { blog }
---
const posts = await getCollection('blog', ({ data }) => !data.draft)
const { Content } = await render(posts[0])
---
<Content />
---
import { Image, Picture } from 'astro:assets'
import hero from '../assets/hero.png'
---
<Image src={hero} alt="Hero" width={1200} height={600} format="webp" />
<Picture src={hero} formats={['avif', 'webp']} widths={[400, 800, 1200]} alt="Responsive" />
// astro.config.mjs
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
})
Use @astrojs/netlify, @astrojs/vercel, @astrojs/cloudflare adapters. For SSG, output goes to dist/.
Image / Picture from astro:assets for optimized images.No artifact produced. Next skill: frontend-universal-seo for meta, sitemap, structured data. Or frontend-universal-pwa for service worker. Carry forward: island strategy, content collection schema, adapter config.