Building Static Sites on Fyso
Recommended stack, project setup, and deployment patterns for Fyso's static site hosting.
Recommended Stack
| Tool | Version | Why |
|---|---|---|
| Astro | 5.x | Zero-JS by default, component islands, fast builds, .astro templates |
| Tailwind CSS | 3.x | Utility-first CSS, no runtime, purged in production |
| TypeScript | 5.x | Type safety for data files and components |
This combination produces small, fast static sites with no client-side JavaScript unless you explicitly opt in. Astro's output: 'static' mode generates plain HTML files — exactly what Fyso's file server expects.
Why not Next.js, Vite, or Hugo?
They all work. Fyso serves any static HTML from a ZIP file. But Astro + Tailwind is the sweet spot:
- Next.js requires
output: 'export'and loses SSR/API routes. If you need those, use Fyso's API directly. - Vite (vanilla) works for SPAs but requires manual routing setup.
- Hugo is fast but uses Go templates — less familiar for TypeScript developers.
- Astro gives you component-based development, data loading at build time, and zero JS shipped by default.
Quick Start
1. Create the project
npm create astro@latest my-site
cd my-site
npx astro add tailwind
This installs Astro, creates the project structure, and configures Tailwind CSS.
2. Verify the config
astro.config.mjs:
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
export default defineConfig({
integrations: [tailwind()],
output: 'static',
});
tailwind.config.mjs:
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
3. Build and deploy
npm run build
# Output goes to dist/
Deploy via MCP (from Claude Desktop or Claude Code):
> "Deploy my site from /path/to/my-site/dist to subdomain 'my-site'"
Or via curl:
cd my-site
zip -r dist.zip dist/
curl -X POST https://app.fyso.dev/api/sites/my-site/deploy \
-H "Authorization: Bearer $FYSO_API_KEY" \
-F "file=@dist.zip"
Your site is live at https://my-site.sites.fyso.dev.
Project Structure
my-site/
├── src/
│ ├── pages/
│ │ └── index.astro # Routes = file paths
│ ├── layouts/
│ │ └── Layout.astro # HTML wrapper (head, body)
│ ├── components/
│ │ ├── Header.astro # Reusable components
│ │ └── Footer.astro
│ ├── data/
│ │ └── content.json # Build-time data (loaded in frontmatter)
│ └── styles/
│ └── global.css # Global styles (Tailwind directives)
├── public/ # Static assets (copied as-is)
├── astro.config.mjs
├── tailwind.config.mjs
├── package.json
└── tsconfig.json
Key conventions
- Pages live in
src/pages/. File paths become URL paths:src/pages/about.astro→/about. - Layouts wrap pages with shared HTML structure (head tags, nav, footer).
- Components are
.astrofiles with scoped styles and zero JS overhead. - Data files in
src/data/are imported in frontmatter and available at build time.
Patterns
Loading data at build time
Astro components have a frontmatter block (between --- fences) that runs at build time:
---
// This runs at build time, not in the browser
import items from '../data/items.json';
const sorted = items.sort((a, b) => b.date.localeCompare(a.date));
---
<ul>
{sorted.map(item => (
<li>{item.title} — {item.date}</li>
))}
</ul>
Fetching from Fyso API at build time
You can pull data from your Fyso entities during the build:
---
const API_URL = import.meta.env.FYSO_API_URL || 'https://app.fyso.dev/api';
const API_KEY = import.meta.env.FYSO_API_KEY;
const res = await fetch(`${API_URL}/entities/products/records?limit=50`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const { data: products } = await res.json();
---
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{products.map(p => (
<div class="p-4 border rounded">
<h3>{p.data.name}</h3>
<p class="text-gray-600">{p.data.description}</p>
<span class="font-bold">${p.data.price}</span>
</div>
))}
</div>
Important: Entity fields are nested inside record.data, not at the top level. Access them as p.data.name, not p.name.
Set environment variables in a .env file (not committed):
FYSO_API_URL=https://app.fyso.dev/api
FYSO_API_KEY=fyso_ak_your_key_here
Custom color palette
Extend Tailwind with your brand colors:
// tailwind.config.mjs
export default {
content: ['./src/**/*.{astro,html,js,ts,tsx}'],
theme: {
extend: {
colors: {
brand: '#3b82f6',
surface: '#1e1e2e',
text: '#cdd6f4',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
};
Then use them in your templates:
<div class="bg-surface text-text">
<h1 class="text-brand">My Site</h1>
</div>
Multi-page sites
Add pages as .astro files in src/pages/:
src/pages/
├── index.astro → /
├── about.astro → /about
├── contact.astro → /contact
└── blog/
├── index.astro → /blog
└── first-post.astro → /blog/first-post
Fyso's Caddy config handles try_files with .html extension fallback, so /about correctly serves about.html.
Responsive design
Use Tailwind breakpoints. No JavaScript needed:
<!-- Stack on mobile, grid on desktop -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="p-4">Card 1</div>
<div class="p-4">Card 2</div>
<div class="p-4">Card 3</div>
</div>
<!-- Show/hide by screen size -->
<nav class="hidden md:flex">Desktop nav</nav>
<nav class="md:hidden">Mobile nav</nav>
Prefer CSS breakpoints (hidden/md:block) over JavaScript media queries. No hydration issues, no JS dependency.
Deployment
Via MCP (recommended)
The deploy_static_site MCP tool handles everything:
- Build your site:
npm run build - Tell your agent: "Deploy my site from
/path/to/distto subdomainmy-site"
The MCP tool zips the directory, uploads it, and returns the URL.
Via curl
npm run build
cd dist && zip -r ../site.zip . && cd ..
curl -X POST https://app.fyso.dev/api/sites/my-site/deploy \
-H "Authorization: Bearer $FYSO_API_KEY" \
-F "file=@site.zip"
rm site.zip
Via Makefile
Add a deploy target to your Makefile:
SUBDOMAIN ?= my-site
API_KEY ?= $(shell echo $$FYSO_API_KEY)
deploy: build
@cd dist && zip -qr /tmp/_deploy.zip . && cd ..
@curl -s -X POST "https://app.fyso.dev/api/sites/$(SUBDOMAIN)/deploy" \
-H "Authorization: Bearer $(API_KEY)" \
-F "file=@/tmp/_deploy.zip" | jq .
@rm /tmp/_deploy.zip
build:
npm run build
Redeployment
Deploying to an existing subdomain replaces the previous version. The deployment is atomic — users see either the old or new version, never a partial state.
Badge
Fyso injects a small "Creado con Fyso" badge into HTML files during deployment. This is automatic for free tier sites. The badge is a fixed-position element that links back to Fyso.
Limits
| Limit | Value |
|---|---|
| Compressed ZIP size | 50 MB |
| Decompressed size | 500 MB |
| Subdomain length | 1-63 characters |
| Subdomain characters | a-z, 0-9, - |
| Reserved subdomains | www, api, app, admin, mail, ftp, staging, test, dev, static, assets, cdn |
Tips
- Keep builds small. Astro + Tailwind produces tiny bundles. Avoid shipping unnecessary assets.
- Use
public/for static assets. Files inpublic/are copied directly — images, fonts, favicons. - Data at build time, not runtime. Fetch from Fyso API in frontmatter, not in client-side JavaScript. This keeps your site fast and your API key out of the browser.
- Rebuild to update. Since the site is static, redeploy after data changes. For frequently changing data, consider client-side fetch with a public API endpoint instead.
- No server-side code. Fyso serves static files only. For dynamic behavior, use Fyso's REST API from client-side JavaScript or build an API-backed SPA.
Reference Implementation
The Cero dashboard at cero.sites.fyso.dev is built with this stack:
- Astro 5.3.0 + Tailwind CSS 3.4.17
- Custom color palette (dark theme with warm tones)
- Data-driven content loaded from JSON files at build time
- Multi-language support (ES/EN/JA) via localStorage
- 8 components, zero client-side framework dependencies
- Deployed via MCP
deploy_static_sitetool