A lean, production-ready Astro v7 monorepo template with Tailwind v4, Svelte 5 and Biome.
Website: astro-template.casoon.de · Live Demos: Starter · Blog
Status: Astro v7 is stable. This template tracks the latest Astro v7 releases.
This template succeeds astro-v6-template.
| Area | v6 | v7 |
|---|---|---|
| Build system | Vite 7 + Rollup | Vite 8 + Rolldown (Rust, 15–61% faster) |
.astro compiler |
Go-based | Rust-based (stricter HTML, all tags must close) |
compressHTML default |
true |
'jsx' (JSX-style whitespace stripping) |
| Markdown processor | remark/rehype | Sätteri (Rust, GFM + heading IDs built-in) |
remark-gfm / rehype-slug |
Explicit plugins needed | Removed — Sätteri provides both natively |
@astrojs/cloudflare |
v13 | v14 |
@astrojs/mdx |
v6 | v7 |
@astrojs/svelte |
v8 | v9 |
src/fetch.ts |
User-defined file | Reserved for advanced routing |
| Advanced routing | Experimental | Stable (no experimental.advancedRouting flag) |
| Dev server | Standard | Background mode: astro dev --background |
Rust compiler — all HTML tags must be closed
The new Rust .astro compiler is strict about HTML. All non-void elements must have a matching closing tag — unclosed tags that previously rendered fine will now produce build errors.
compressHTML: 'jsx'
Whitespace is stripped using JSX rules instead of HTML-aware compression. This can collapse the space between adjacent inline elements:
<!-- renders "helloworld" with v7 default, not "hello world" -->
<span>hello</span><em>world</em>Fix: add {" "} between inline elements, or override with compressHTML: true. This template sets compressHTML: 'jsx' explicitly so the behavior is visible in config.
Sätteri replaces remark/rehype
Astro v7 ships Sätteri, a Rust-powered markdown processor, as the default. GitHub Flavored Markdown (tables, strikethrough, etc.) and heading anchor IDs (rehype-slug behavior) work out of the box — no plugins needed. If you need custom remark/rehype plugins, install @astrojs/markdown-remark and configure markdown: { processor: unified() }.
@astrojs/cloudflare v14
The v14 adapter removed the workerEntryPoint and cloudflareModules config options. The adapter now runs workerd at every stage (dev, prerender, production). If you were using these options in v13, remove them.
src/fetch.ts is reserved
Astro v7 uses src/fetch.ts as the entry point for advanced routing. Rename any existing file with that name, or configure fetchFile: null in astro.config.mjs.
This template uses pnpm workspaces with a shared library, making it an ideal foundation for running multiple projects from a single repository. Typical use cases:
- Main site + Blog (included as
starterandblog) - Landing pages for campaigns or product launches
- Online shop storefront
- Documentation site
All projects share the same design tokens, UI components and utilities — ensuring a consistent look and feel while keeping each app independently deployable. Adding a new project is as simple as creating a new folder under apps/ and importing from shared/.
- Astro v7 — Vite 8 + Rolldown, Rust compiler, Sätteri markdown, stable advanced routing
- Tailwind v4 — CSS-first config, Vite plugin, OKLCH colors
- Svelte 5 — Runes API ($state, $derived) for reactive islands
- i18n — Multi-language support (en/de) with Astro i18n routing
- OG Images — Auto-generated Open Graph images at build time (Satori + resvg)
- Astro Actions — Server-side contact form handling
- CSP — Content Security Policy (
security.csp): scripts and styles by SHA-256 hash or same origin, everything else'self' - Build Metrics —
@casoon/astro-speed-measurefor build performance tracking - Post-Build Audit —
@casoon/astro-post-auditfor SEO, link and WCAG checks after every build - Code Analysis — fallow for dead code, complexity hotspots and duplication detection
- prop-for-that — reactive CSS custom properties (pointer, scroll, viewport, form state, ...) via a toggleable integration, demoed on the starter homepage
- Web Vitals —
@casoon/astro-webvitalsRUM in production, reporting sampled Core Web Vitals through a platform-independent, typed API route - Secret Scanning —
@casoon/nosecretsin pre-commit plus manual workspace scans - Playwright — E2E tests for both apps with axe-core a11y scanning
- Biome — Single tool for linting + formatting (replaces ESLint + Prettier)
- Zod v4 — Runtime validation for env and forms
- pnpm Workspaces — Monorepo with catalog for centralized dependency management
- Dark Mode — System preference + manual toggle
- Site Files —
@casoon/astro-site-filesgeneratesrobots.txt,sitemap.xml,llms.txtand (withPUBLIC_SECURITY_CONTACTset)security.txtat build time - Structured Data —
@casoon/astro-structured-datagenerates JSON-LD via typed Astro components - SEO — robots.txt, canonical URLs, meta descriptions, Open Graph
- WCAG 2.1 AA — Two-layer accessibility: axe-core runtime checks + static HTML audit
- TypeScript Strict — Fully typed throughout
astro-v7-template/
├── apps/
│ ├── starter/ # Landing page + contact form + i18n
│ └── blog/ # Blog with MDX + RSS + i18n
├── shared/ # Design tokens, components, layouts, SEO, utilities, shared Astro config
├── integrations/ # Custom Astro integrations (e.g. prop-for-that)
├── e2e/
│ ├── starter/ # Playwright E2E tests for starter
│ └── blog/ # Playwright E2E tests for blog
├── .github/workflows/ # CI pipeline
├── biome.json # Linting & formatting
├── playwright.config.ts # E2E test configuration
└── pnpm-workspace.yaml # Workspace + catalog
- Node.js >= 22.12.0
- pnpm >= 11.0.0
git clone https://github.com/casoon/astro-v7-template.git
cd astro-v7-template
pnpm install
# Start the starter app
pnpm dev
# Start the blog app
pnpm dev:blogTip: opi lists and runs these scripts from an interactive terminal menu, with the descriptions from
scripts-infoinpackage.json. It also bundles the checks:opi --check commitruns lint, types and the secret scan before a commit.
| Script | Description |
|---|---|
pnpm dev |
Start the starter app (port 5014) |
pnpm dev:blog |
Start the blog app (port 4322) |
pnpm build |
Build all apps |
pnpm build:starter |
Build starter only |
pnpm build:blog |
Build blog only |
pnpm preview |
Preview the built starter app |
pnpm preview:starter |
Preview the built starter app |
pnpm preview:blog |
Preview the built blog app |
pnpm check |
Run Biome lint + format check |
pnpm check:fix |
Biome auto-fix |
pnpm format |
Format all files |
pnpm test:unit |
Unit tests for shared utilities (node --test, no extra dependencies) |
pnpm test:e2e |
Run all Playwright E2E tests (build first; starter runs in wrangler dev, blog on a static server) |
pnpm test:e2e:starter |
E2E tests for starter only |
pnpm test:e2e:starter:sitemap-rum |
Verify RUM transport for every starter sitemap URL |
pnpm audit:pages |
Build and check page metadata and basic accessibility signals for every sitemap URL |
pnpm audit:pages:dev |
Run the same page checks against astro dev |
pnpm test:e2e:blog |
E2E tests for blog only |
pnpm type-check |
TypeScript check |
pnpm fallow:dead |
Detect unused files, exports and dependencies |
pnpm fallow:health |
Complexity score, hotspots and refactor targets |
pnpm fallow:audit |
Combined dead-code + complexity + duplication audit |
pnpm secrets:scan |
Scan the workspace for leaked secrets |
pnpm secrets:scan:staged |
Scan only staged files for leaked secrets |
pnpm deploy |
Build and deploy both apps to Cloudflare Workers |
pnpm deploy:starter |
Build and deploy the starter app |
pnpm deploy:blog |
Build and deploy the blog app |
pnpm clean |
Remove build artifacts + node_modules |
- TypeScript pinned to
^6.0.3.@astrojs/check(via@astrojs/language-server) restricts itstypescriptpeer to^5.0.0 || ^6.0.0— TypeScript 7's native compiler doesn't yet expose the programmatic APIastro checkrelies on. Revisit once Astro's language server supports it (withastro/roadmap#1321).
Landing page featuring:
- Hero section with feature grid
- Contact form with Astro Actions + Zod validation and a honeypot field — ships as a demo:
deliverContactMessage()insrc/actions/contact.tsis the hook for your mail provider; until it returnstrue, the page tells visitors that nothing was sent - i18n (English + German) with language switcher
- OG image generation per page and locale
- Dark mode toggle
- SEO component with JSON-LD
- prop-for-that demo section (pointer tilt, scroll reveal, range/clock/viewport/size/truncation/color-input sources)
- Web Vitals RUM (
@casoon/astro-webvitals) with a platform-independent analytics endpoint
Blog template featuring:
- MDX support with Sätteri (GFM + heading IDs built-in), code block language labels and Shiki title transformer
- Content Collections (Loader API) with schema fields for
readTime,articleType,seriesandseriesOrder - Table of Contents — sticky sidebar on desktop with IntersectionObserver scrollspy
- Reading progress bar — thin indicator at the top of every post
- Enriched RSS feed (
/rss.xml) witharticle:readingTime,article:seriesand OG image enclosures - Automatic sitemap (
/sitemap.xml) with i18n hreflang via@casoon/astro-site-files - i18n (English + German) with language switcher — one folder per locale (
src/content/blog/en/,src/content/blog/de/), same file name = same post in both languages - OG image generation per page, blog post and locale
- Tag display
prop-for-that exposes runtime state (pointer, scroll, viewport, form state, ...) as CSS custom properties, so effects can be driven with plain CSS instead of JavaScript. It's wired up as a template-wide integration (integrations/props-for-that.mjs) rather than a shared component, so any project cloned from this template gets it for free.
- Registered per app in
astro.config.mjsviapropsForThat({ enabled: true })— setenabled: falseto strip the injected script from a clone that doesn't need it. - The auto-bootstrap is a no-op until an element carries a
data-props-forattribute, so pages that don't use it pay no runtime cost. - See the starter homepage for a live demo, and
shared/src/styles/global.cssfor the corresponding.props-*utility classes.
@casoon/astro-webvitals v0.4.9 measures Core Web Vitals (LCP, CLS, INP) plus FCP/TTFB in the browser and reports them through a sampled, batched endpoint:
apps/starter/src/layouts/StarterLayout.astroapplies<WebVitals debug={import.meta.env.DEV} endpoint="/api/analytics/vitals/" sampleRate={import.meta.env.DEV ? 1 : 0.1} dashboard />to every starter page. The overlay appears only in development; every local session and 10% of production sessions report metrics.apps/starter/src/pages/api/analytics/vitals.tsvalidates the batch payload with Zod and returns204. It has no Cloudflare runtime import, so it works unchanged with Node, Vercel, Netlify and Cloudflare adapters.- The default endpoint intentionally does not persist analytics data. Add your database, queue or analytics-provider call after validation; this keeps the template portable and makes the data destination an explicit choice.
- Each report contains
id,delta,ratingandnavigationTypewhen available. Aggregatedeltabyid; supporting metrics such as Long Tasks and navigation timings legitimately omit a rating. - Optional package features are
attribution,trackLongTasks,trackSoftNavigations,retryFailedMetrics,consentandrespectDnt. Enable them only after defining the corresponding data and privacy policy. apps/starter/astro.config.mjsuseswebVitalsDashboard({ route: '/web-vitals' })from@casoon/astro-webvitals/integrationto inject the noindex route/web-vitals/. It is excluded from the sitemap and Post-Audit because it is an internal tool. It reads at most 200 local metrics from this browser's storage, has clear and sitemap-pass controls, and requires neither Cloudflare nor a database.- Open
/web-vitals/after visiting a starter page to inspect the latest local metrics per URL. Run sitemap pass visits each page in/sitemap.xmlin this tab, waits for local measurements and returns to the dashboard.astro devsupplies a matching fallback for staticsrc/pagesroutes; preview and production use the generated sitemap. It is browser-local QA data, not shared or representative production analytics.
@casoon/astro-post-audit is the static counterpart to Web Vitals: it checks rendered SEO, links and lightweight accessibility rules at build time, while Web Vitals captures what users experience in the browser. The starter's build runs both layers.
Both apps configure @casoon/astro-post-audit (v0.9.1) through postAuditOptions() in shared/src/config/astro.ts: preset: 'standard' with failOn: 'errors' (only real errors fail the build; advisory/info findings do not), progress: 'verbose', hints: { sourceFiles: true } to map findings back to source files, and contentStyle: true for heuristic tone/language checks. The rules block enables canonical.self_reference, opengraph.require_og_image, a11y.require_skip_link, structured_data.check_json_ld, html_validation.enabled, css_architecture.enabled (per-route CSS payload and outlier detection), duplicate title/description detection under content_quality, links.check_fragments and links.known_routes (on-demand routes such as the starter's contact pages, which have no file in dist/), with severity overriding html/assertion.roles.unnecessary-list to off (a false positive for this template's semantic list markup). rules.filters.exclude skips 404.html on both apps, plus web-vitals/index.html (starter — noindexed dashboard route) and blog/index.html (blog — an empty Astro.redirect('/') stub, which post-audit would otherwise flag as a 0-byte file).
e2e/starter/web-vitals.spec.ts verifies the analytics contract for an official vital and a supporting metric, then confirms browser transport, the local dashboard route and the sitemap pass returning after every sitemap page. The endpoint contract is tested without requiring a Cloudflare Analytics Engine binding.
Run pnpm test:e2e:starter:sitemap-rum for the opt-in sitemap RUM mode. It gets every URL from /sitemap.xml, opens it in a browser and requires a valid batched TTFB report. The normal E2E commands exclude this broader browser pass.
Run pnpm audit:pages for a terminal report of the browser-visible page checks collected by the Web Vitals dashboard. It builds the starter first and uses the generated sitemap, so it is suitable for CI. pnpm audit:pages:dev starts (or reuses) the starter dev server and checks the static-page sitemap fallback for a fast local loop. Restart an already running dev server after upgrading @casoon/astro-webvitals, so Vite loads the new browser bundle. Both commands exit with an error when a page has an issue.
Both homepages (/#metadata-demo, /de/#metadata-demo) show links to generated site files and a visible FAQ. The same question/answer data produce FAQPage JSON-LD; the organization profile and team credits are explicitly fictional examples.
| Output | Demonstration |
|---|---|
/sitemap.xml |
Localized URLs, hreflang pairs, priorities and change frequencies; blog entries carry frontmatter lastmod dates. Contact routes are included despite SSR. Internal tools and redirects are excluded. Duplicate URLs fail the build. |
/robots.txt |
seoOnly allows search engines and blocks known AI crawlers; the starter also excludes /api/ and /web-vitals/. |
/llms.txt |
Site overview, context and absolute links. The blog adds separate English/German article sections from published frontmatter via llms.sources; drafts are omitted. |
/humans.txt |
Fictional team credits, acknowledgments and technology stack from siteCredits in shared/src/config/site.ts. |
/rss.xml, /de/rss.xml (blog) |
English feed via the existing Astro RSS route; German feed generated by the astro-site-files build-time rss option. |
/.well-known/security.txt |
Optional: set PUBLIC_SECURITY_CONTACT=mailto:security@your-domain.example to generate contact, canonical URL, languages and a rolling expiry. Replace the example address with your real contact. |
llms.txt is descriptive and does not override robots.txt. The template retains its existing seoOnly policy. If a project should allow AI retrieval/citation, choose a different preset deliberately; available presets are documented in astro-site-files.
BaseLayout.astro uses the typed WebPageSchema, WebSiteSchema, OrganizationSchema and ArticleSchema components. Stable @id values link pages, the site and the publisher. Page languages drive inLanguage and og:locale, and WebPage alternates generate HTML hreflang links. Blog articles demonstrate keywords, image dimensions, word counts and a reading time matching the visible article header; AutoBreadcrumbSchema adds the breadcrumb list.
MetadataDemo.astro demonstrates the generic Schema component with a FAQPage. It includes the page title, description, image and language because FAQPage takes precedence over WebPage for generated metadata, while the typed FAQSchema only accepts Q&A props. The visible FAQ and JSON-LD share one data source. There is no added client JavaScript. Valid JSON-LD does not guarantee a rich result in search.
Site files are generated by pnpm build; use preview or the built output to inspect them. Security output remains disabled until a contact is configured. The source of truth is each app's astro.config.mjs, plus shared/src/config/site.ts and the blog frontmatter helpers in apps/blog/src/utils/blog-rss.js.
Use apps/starter or apps/blog as the starting point, depending on whether the new project is page-focused or content-focused.
- Create
apps/<name>/with its ownpackage.json,astro.config.mjs,tsconfig.json,src/andpublic/folders. - Set the package name to
@astro-v7/<name>and add scripts fordev,build,preview,type-check,cleananddeploy. - Import shared styles from
@astro-v7/shared/styles/global.cssin the app layout. - Copy the app-level
tsconfig.jsonpath aliases so@/*points to localsrc/*and@astro-v7/shared/*points to../../shared/src/*. - Configure
astro.config.mjs: spreadbaseAstroConfigand useviteSsrConfig()/postAuditOptions()from@astro-v7/shared/config/astro, then add the core integrations: Tailwind, Svelte,@casoon/astro-site-files,@casoon/astro-structured-data,@casoon/astro-post-auditand@casoon/astro-speed-measure. - Choose a unique dev port if the app should run alongside the existing apps.
- Add root scripts such as
dev:<name>,build:<name>andpreview:<name>from the workspace root. - Add an optional Playwright project in
playwright.config.tsplus tests undere2e/<name>/.
The template ships pre-configured for Cloudflare Workers. pnpm deploy:starter and pnpm deploy:blog each build the app and run wrangler deploy:
pnpm deploy:starter
pnpm deploy:blog
# or both apps at once
pnpm deployBefore the first deploy of your own project:
-
Names — set
nameinapps/*/wrangler.toml. -
Site URL — set
PUBLIC_SITE_URL(andPUBLIC_SITE_NAME) in.envor the defaults inapps/*/src/env.ts; canonical URLs, sitemap and OG images are built from it. -
Custom domain — uncomment the
[[routes]]block inapps/starter/wrangler.toml; without it the Worker runs on*.workers.dev. -
Sessions — off by default (
session: falseinshared/src/config/astro.ts). Remove that line to useAstro.session; the Cloudflare adapter then binds aSESSIONKV namespace, whichwrangler deployprovisions on first deploy. -
security.txt — set
PUBLIC_SECURITY_CONTACT(mailto:orhttps:) to publish/.well-known/security.txt;Expiresis rolled forward on every build. -
Site profile — replace the demo organization data (email, phone, address,
sameAs, launch date) inshared/src/config/site.tsand the placeholderpublic/logo.svgin each app; they feed the JSON-LD on every page. -
Contact form — connect a provider in
deliverContactMessage(). The honeypot only stops simple bots; once messages are actually sent, add rate limiting (e.g. a Cloudflare rate-limiting rule or Workers Rate Limiting binding) or a Turnstile check before delivery. -
Content Security Policy — the default policy only allows the own origin (plus
data:images and inlinestyleattributes). Loading external fonts, analytics, embeds or a Turnstile widget needs matching directives; extend them in the app'sastro.config.mjs:security: { ...baseAstroConfig.security, csp: { ...baseAstroConfig.security.csp, directives: [...baseAstroConfig.security.csp.directives, "frame-src https://challenges.cloudflare.com"], }, },
The option must stay under
security.csp— a top-levelcspkey is silently ignored by Astro, and the site then ships without any policy. Scripts in<head>must be files inpublic/(liketheme-init.js), not inline: Astro inserts the CSP<meta>after them, so they would run unprotected.e2e/*/csp.spec.tsfails on any violation.
Shared Astro options (i18n, CSP, image service, post-audit rules, the vite.ssr.optimizeDeps pin list) live in shared/src/config/astro.ts; each app's astro.config.mjs only adds its adapter, integrations and app-specific values.
Three config changes per app:
- Swap the adapter in
astro.config.mjs(@astrojs/node,@astrojs/vercel,@astrojs/netlify) - Enable Sharp — override
image.servicewith'astro/assets/services/sharp'in the app config (the shared base sets the noop service for Workers) - Delete
wrangler.toml— not needed outside Cloudflare
- Vite 8 + Rolldown — Rust-based bundler, 15–61% faster builds on real-world sites
- Rust
.astrocompiler — Faster compilation, stricter HTML validation - Sätteri markdown — GFM and heading anchors without plugins
compressHTML: 'jsx'— Explicit whitespace mode, visible in config- Stable advanced routing — No more
experimental.advancedRoutingflag @astrojs/cloudflarev14 — Full workerd parity across dev, prerender and productionastro dev --background— Background dev server for AI coding agents and CI workflows
MIT