Installation
Grafio ships as a complete Astro project, not as an npm package or an integration you register. You clone the repository, install its dependencies, and start editing — there is no theme layer sitting between you and the markup, and nothing is hidden inside node_modules. Every component, token, icon and animation is a file in src/ that you own from the first commit.
It runs with no configuration, no API keys and no accounts. pnpm install followed by pnpm dev gives you the full site — home, about, work index, case studies, blog, contact, legal pages — populated with sample content. Configuration is what you do once you have looked around, not a prerequisite for seeing it work.
Requirements
- Node.js 22.12.0 or newer. This is enforced by
engines.nodeinpackage.json, and it is not a soft floor: the test runner executes TypeScript directly through Node’s--experimental-strip-typesflag, which is only available from 22.12 on. Check yours withnode -v. - pnpm. The lockfile is
pnpm-lock.yaml(lockfile version 9), andpnpm-workspace.yamlcarries install settings the theme relies on. No pnpm version is pinned, so any recent release works. If you do not have it,corepack enable pnpmis the shortest route. - Git, to clone the repository and to keep your own history from the first commit onward.
You do not need a Netlify account, a Resend account, a Cloudflare account, or a domain to run the theme locally. Each of those is optional and covered further down.
Get the code
Clone the repository and drop the upstream history so your project starts with a clean log:
git clone https://github.com/Astro-Craft-Theme/grafio-theme.git my-portfolio
cd my-portfolio
rm -rf .git && git init
If you would rather keep the upstream remote so you can pull theme updates later, skip the rm -rf .git and rename the remote instead:
git remote rename origin upstream
git remote add origin [email protected]:you/my-portfolio.git
Keeping upstream is only worth it if you plan to merge theme changes. Because the theme is source you edit directly, a merge after heavy customization is a real merge — most people take the clean-slate route and treat the clone as a starting point.
Install dependencies
pnpm install
The dependency list is deliberately short. At runtime the theme depends on Astro, Tailwind CSS v4, tailwind-variants and tailwind-merge, two Fontsource variable font families, and three official Astro integrations (@astrojs/mdx, @astrojs/sitemap, @astrojs/netlify). There is no UI kit, no animation library, no SEO package and no icon package — those four things are implemented inside the theme rather than pulled in, which is why the surface you have to keep up to date stays small.
Two details in pnpm-workspace.yaml are worth knowing about before they surprise you:
allowBuildsdenies post-install build scripts for@parcel/watcher,esbuildandsharp. This is intentional. None of the three needs to compile for this project to work, and denying them keeps installs fast and free of native toolchain requirements.minimumReleaseAgeExcludepins the Tailwind packages to the exact version set the theme was built and tested against, including the platform-specific@tailwindcss/oxide-*binaries.
If your install prints a notice about ignored build scripts, that is the allowBuilds setting working as designed, not a failure.
A note on image optimization
sharp is not a dependency here. The Netlify adapter routes astro:assets through Netlify’s Image CDN, so images are optimized at the edge rather than at build time and nothing needs to compile locally. If you later swap the adapter for a host without an equivalent service, Astro falls back to its own image service, which carries sharp as an optional dependency — usually that just works, and pnpm add sharp is the fix if your package manager does not pull it in.
Start the dev server
pnpm dev
The site is at http://localhost:4321. Every route is available immediately with the sample content in place:
/— the home page/about//work/and/work/<slug>/— six sample case studies/blog/and/blog/<slug>/— four sample posts and one author/contact/— the form renders and validates with no keys set/terms/and/privacy/— placeholder legal copy/rss.xml,/robots.txt,/llms.txt,/sitemap-index.xml— all generated, all deriving their absolute URLs from one setting
The primitive catalog
Before you start building pages, open http://localhost:4321/examples/ui. It is a development-only catalog of all 44 UI primitives in every variant, alongside the 91 motion utilities and the 571-icon set. It is the fastest way to find out what already exists rather than rebuilding it.
The route is gated by import.meta.env.PROD inside getStaticPaths, so a production build emits no paths for it and no HTML ships. You can leave it in place while you work. When you no longer need it, delete src/pages/examples/ and src/components/Sections/UiCatalog/ together — Tailwind still scans the catalog’s markup even though it builds no pages, and its demo classes sit in the stylesheet every real page loads. Removing it takes the shared CSS from roughly 108 KB to 89 KB.
What runs where
Every page is prerendered to static HTML with exactly one exception: src/pages/contact.astro sets export const prerender = false; because the form posts to a server action. That single route is the only thing the adapter ever runs.
This matters at install time for one reason. Under astro dev everything runs on a server, so a route that accidentally depends on request-time behavior will work locally and fail in a static build. Run pnpm build early and often — it is the check that tells the truth.
Environment variables
None are required. A fresh clone builds and runs with an empty environment, and the contact form still renders, validates input and reports a readable message instead of failing silently. Set these when you want the corresponding feature to work.
Copy the example file and fill in what you need:
cp .env.example .env
.env and .env.production are already gitignored.
The five variables
SITE_URL— your production domain. It feeds the canonical link, Open Graph tags, JSON-LD, the sitemap,robots.txt,llms.txtand the RSS feed, so one wrong value poisons six things at once. It defaults tohttps://example.com, and a Netlify production build throws if it is still the placeholder. Local builds and deploy previews are unaffected, which is why this only ever bites at deploy time. Note that.env.exampledoes not list it — set it in your host’s environment rather than in the repo.RESEND_API_KEY— a Resend API key. Required for the contact form to deliver mail.CONTACT_TO_EMAIL— where enquiries land. Also required for delivery.CONTACT_FROM_EMAIL— optional, defaults to[email protected]. That shared sender works immediately but can only deliver to the address that owns the Resend account, so ifCONTACT_TO_EMAILis a different address, delivery fails at the provider even though everything else is correct. Verify your own domain in Resend and set this to send anywhere.TURNSTILE_SITE_KEYandTURNSTILE_SECRET_KEY— optional Cloudflare Turnstile keys.
RESEND_API_KEY=re_xxxxxxxxxxxx
CONTACT_TO_EMAIL[email protected]
CONTACT_FROM_EMAIL[email protected]
How the form behaves with nothing set
The form is functional from the first run. It validates on the server, works with JavaScript disabled, and is protected by a honeypot field and a three-second time gate regardless of configuration. With RESEND_API_KEY or CONTACT_TO_EMAIL missing, a submission returns a form-level message saying the form is not configured yet — a real error you can see, not a silent drop. That check happens at request time, so a missing key never breaks your build.
Turnstile is both keys or neither
Turnstile enforcement turns on only when both keys are present. Setting one logs a warning and leaves verification off, which is deliberate: a half-configured challenge that blocks real visitors is worse than no challenge. Be aware of the trade-off before you enable it — the Turnstile widget requires JavaScript, so setting the keys makes your contact form JS-only. Leaving them unset keeps the no-JS path, which the honeypot and time gate still cover.
Make it yours
With the site running, these are the first files to edit. All of them are typed, so a mistake fails the build with the offending value named rather than shipping quietly.
src/config/siteData.json.ts— brand name, page title, description, and the author block: name, email, tagline, location, IANA time zone (it drives the live clock in the footer) and thesocialslist. The JSON-LDsameAsarray is derived fromsocials, so you edit one list and structured data follows. Two build-time guards run when this module is imported: the author email must parse as an email, and every social link must be an absolutehttp(s)URL.src/config/siteSettings.json.ts—siteLangandsiteLocale, plus three switches:useViewTransitions,useAnimations(the master switch for decorative motion) anddefaultTheme, which is"dark"out of the box. Note thatprefers-reduced-motionis honored by a global CSS guard no matter whatuseAnimationssays; the flag is a design choice, not an accessibility one.src/config/legalData.json.ts— the terms and privacy copy. It is explicitly placeholder text. Have a professional review it; it is not legal advice.public/og.jpg,public/favicon.svg,public/favicon.ico— the shippedog.jpgis a labelled placeholder. Replace it with a real 1200×630 image.src/data/— replace the six work entries, four blog posts and one sample author. Each entry is a folder whose name is the URL slug, holding anindex.mdxand its own images. Schemas live insrc/content.config.ts, so bad frontmatter fails the build with the entry named — aworkentry, for instance, requirestitle,discipline,order,thumbnail,standfirst,year,stack,roleand achaptersarray, with nothing optional.
Colors, type and spacing are not in this list because they are not in the config layer. They are CSS tokens in src/styles/tailwind-theme.css, and the Colors and Typography pages cover them.
Verify your install
pnpm lint && pnpm check && pnpm build && pnpm test
pnpm lintruns ESLint, including the JSX accessibility rules.pnpm checkrunsastro checkfor types across.astroand.tsfiles.pnpm buildis the real check. Content-schema errors, config typos and broken references all surface here rather than in the browser.pnpm testruns every*.test.tsfile undersrc/with Node’s type stripping — no framework, no fixtures, nothing to register. There are eleven such files today, covering the header scroll rule, contact validation, the JSON-LD builders, the RSS feed and more. If it ever finds zero test files it fails rather than passing, so the suite cannot quietly disappear.
There is also pnpm format, which runs eslint --fix and then Prettier. Class ordering is handled by prettier-plugin-tailwindcss, so let it sort and do not hand-order class lists.
Troubleshooting
The build throws about SITE_URL. You are running a Netlify production build with the placeholder domain still in place. Set SITE_URL under Site configuration → Environment variables. This is intentional — it makes it impossible to ship canonical URLs and a sitemap pointing at example.com.
A renamed content entry keeps 404ing in dev. Astro’s content layer caches entries, and moving or renaming a folder while the dev server is running can leave the old entry in place and the new one missing. Restart pnpm dev; touching the file will not clear it.
A utility class has no effect in dev. In a long-running dev server, classes used only in newly created files can be missing from the generated stylesheet. Restart the dev server before you go looking for a bug in your markup.
The contact form says it is not configured. That is the expected message when RESEND_API_KEY or CONTACT_TO_EMAIL is unset. See the environment section above.
Mail sends without error but never arrives. You are almost certainly still on the default [email protected] sender, which can only deliver to the address that owns your Resend account. Verify a domain in Resend and set CONTACT_FROM_EMAIL.
Node version errors on install. The floor is 22.12.0. Older releases will fail on the test runner even if the install itself appears to succeed.