A minimal static starter with a full markdown blog engine, Google Consent Mode v2, datalayer events and SEO/metadata
layer. Everything reads from plain TypeScript config
(src/site.config.ts + src/config.ts).
Built and verified against Astro 7.0 + Tailwind 4.3, on Vite 8 (Node ≥ 22.12).
Use this folder directly. It's a complete, buildable project:
npm install
npm run dev # http://localhost:4321
npm run build # static output in ./distVite alignment. Astro 7 runs on Vite 8, the same major
@tailwindcss/viteuses, so nooverridespin is needed (unlike Astro 6, which needed Vite held at 7). If you ever see a strayoverrides.viteentry, delete it.Markdown pipeline. Astro 7 defaults to the new Rust "Sätteri" processor, which does not run remark/rehype plugins. This starter keeps the reading-time and responsive-table plugins working by explicitly opting into the unified pipeline via
processor: unified(...)from@astrojs/markdown-remarkinastro.config.ts. Leave that in place unless you port the plugins to Sätteri.
A neutral Header (logo left, nav + "Contact us" CTA right, Alpine mobile menu),
Footer, and pop-up cookie banner (Accept all / Reject all + granular options)
are included. Links live in src/navigation.ts; the logo is a placeholder in
src/components/Logo.astro.
src/site.config.ts→ setSITE.siteto your real origin andname. This is the only place the domain lives —astro.config.tsimports it androbots.txtis generated from it at build time.src/config.ts→METADATAdefaults and the blog options.- Replace the placeholder icons (
public/favicon.svg,favicon.ico,apple-touch-icon.png,icon-192.png,icon-512.png) andsrc/assets/images/default.png(your 1200×630 social-card fallback). src/config.ts→ setCONTACT.formspreeEndpointto enable the contact form.
Blog (data layer — design-agnostic):
src/content.config.ts— Zod schema for posts insrc/data/post/.src/utils/blog.ts—fetchPosts, related posts, andgetStaticPaths*for list / post / category / tag, all with pagination.src/utils/permalinks.ts— configurable URL builder (SITE.trailingSlash,APP_BLOG.post.permalinkpattern like/%slug%,%category%,%year%…).src/utils/frontmatter.ts— reading-time remark plugin + responsive-table rehype plugin.src/pages/rss.xml.ts— RSS feed.src/pages/[...blog]/…— minimal placeholder route markup. The data is fully wired; replace the markup inside the<!-- Replace … -->blocks with your own designs.
SEO:
src/components/common/Metadata.astro— wrapsastro-seo; merges site-wide defaults with per-page props → title template, canonical, OpenGraph, Twitter.src/utils/images.ts—findImage(resolve~/assets/images/…) +adaptOpenGraphImages(optimizes the OG image via Sharp to an absolute URL).src/components/common/CommonMeta.astro— charset/viewport, sitemap link, and optional Google site-verification.
Plumbing: @astrojs/sitemap, MDX support, the ~ → src alias, and the
Tailwind v4 setup (@tailwindcss/vite + typography plugin for prose).
Pages are indexable by default (METADATA.robots in src/config.ts).
Per-page and per-route robots override this (e.g. tag archives are noindex
via APP_BLOG.tag.robots).
Posts use a configurable catch-all (src/pages/[...blog]/…). With the default
APP_BLOG.post.permalink = '/blog/%slug%':
/blog,/blog/2… — paginated list/blog/<slug>— a post/category/<slug>,/category/<slug>/2… — category archive/tag/<slug>… — tag archive/rss.xml,/sitemap-index.xml
Change permalink to '/%slug%' for root-level post URLs, or change the
pathname values to rename blog / category / tag.
Add a post by dropping a .md / .mdx file into src/data/post/ with the
frontmatter shown in hello-world.md.
Purely static, deployed via the GitHub connection. No adapter, no
wrangler.toml, no output: 'server' — Pages just builds the repo and serves
dist/. Every push to your main branch is a production deploy; PRs get preview
URLs automatically.
In the Cloudflare dashboard: Workers & Pages → Create → Pages → Connect to Git, pick the repo, then set:
- Framework preset: Astro (or "None" — it only pre-fills the next two)
- Build command:
npm run build - Output directory:
dist(the #1 cause of a blank-page deploy is leaving this aspublic)
Node version is pinned by the committed .nvmrc (22) in this repo, so the
build won't fall back to Cloudflare's older default and fail Astro 7's Node ≥ 22.12
requirement. (Alternatively set a NODE_VERSION = 22 env var in the dashboard.)
Two current gotchas worth knowing:
- Pages vs Workers. Cloudflare is merging the two products and the dashboard
sometimes silently routes a static site to Workers — you'll land on a
*.workers.devURL instead of*.pages.dev. Make sure you went in through the Pages "Connect to Git" path. If you see*.workers.dev, you're on the wrong product. - Don't add a
wrangler.tomlfor this static + Git-connection setup. If one exists it overrides the dashboard settings and can wipe env vars.
Included for static hosting on Cloudflare: public/_headers (immutable caching
for /_astro/* + baseline security headers) and a 404.astro page — Cloudflare
Pages serves dist/404.html automatically for unmatched routes.
(If you ever add server-rendered routes later, that's when you'd install
@astrojs/cloudflare and switch to output: 'server'. Not needed now.)
Docker (optional fallback, not used by the Pages deploy). A multi-stage
Dockerfile builds the site and serves dist/ with nginx:
docker build -t my-site .
docker run -p 8080:80 my-site # http://localhost:8080