Security best practices for deepworkplan.com, a static site built with Astro.
As a static site, deepworkplan.com has a different security profile than dynamic web applications. The main concerns are:
- Build-time secrets - Protecting sensitive data during build
- Client-side exposure - What data reaches the browser
- Third-party dependencies - Supply chain security
- Content security - Protecting against XSS in user-generated content
Static sites ship all client-side code to users. Never include secrets in:
- Astro component scripts
- Svelte component logic
- Client-side JavaScript
// ❌ BAD - Secret exposed to client
const API_KEY = 'sk_live_xxxxx';
// ✅ GOOD - Only public data on client
const SITE_URL = import.meta.env.PUBLIC_SITE_URL;Astro runs code at build time (server-side) and optionally at runtime (API routes). Understand the difference:
| Context | Secrets Safe? | Example |
|---|---|---|
Build-time (.astro frontmatter) |
Fetching data for static pages | |
API Routes (src/pages/api/) |
✅ Yes | Server-side endpoints |
Client-side (Svelte with client:*) |
❌ No | Interactive components |
As a static site:
- No database to protect
- No user authentication
- No session management
- Limited server-side logic
Use .env files for environment variables:
# .env (local development - DO NOT COMMIT)
PUBLIC_SITE_URL=http://localhost:5555
PRIVATE_API_KEY=sk_xxxxx
# .env.production
PUBLIC_SITE_URL=https://deepworkplan.comPUBLIC_*- Safe to expose to client (e.g.,PUBLIC_SITE_URL)- No prefix - Server-only, never reaches client
// Server-side only (build time or API routes)
const privateKey = import.meta.env.PRIVATE_API_KEY;
// Available on client
const siteUrl = import.meta.env.PUBLIC_SITE_URL;- Never commit
.envfiles with secrets - Use
.env.examplefor documentation - Rotate secrets if accidentally exposed
- Use CI/CD environment variables for builds
Blog posts are authored in Markdown/MDX. While you control the content, follow these practices:
<!-- ✅ Safe - standard markdown -->
# My Post
This is safe content.
<!-- ⚠️ Careful with raw HTML in MDX -->
<script>alert('This would execute!')</script>Zod schemas validate content at build time:
// src/content.config.ts
const blog = defineCollection({
schema: z.object({
title: z.string().max(200), // Limit length
description: z.string().max(500),
// Validates structure, prevents malformed data
}),
});When linking to external sites:
<!-- Add rel attributes for security -->
<a href="https://external.com" target="_blank" rel="noopener noreferrer">
External Link
</a>The site has minimal API routes:
| Endpoint | Purpose | Security |
|---|---|---|
/api/posts-en.json |
Blog search index (EN shard) | Public, cached |
/api/posts-es.json |
Blog search index (ES shard) | Public, cached |
/api/posts.json |
Blog search index (compatibility) | Public, cached |
/rss.xml |
RSS feed | Public |
// src/pages/api/posts-en.json.ts (same pattern for posts-es.json)
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
try {
// Validate and sanitize any inputs
// Return only necessary data
const data = await getPublicPosts();
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600',
},
});
} catch (error) {
// Don't expose error details
console.error('API error:', error);
return new Response(JSON.stringify({ error: 'Server error' }), {
status: 500,
});
}
};# Check for known vulnerabilities
npm audit
# Fix automatically where safe
npm audit fix
# Check for outdated packages
pnpm run ncu:checkWhen adding dependencies:
- Check package popularity and maintenance
- Review recent security advisories
- Prefer well-maintained packages
- Minimize dependencies when possible
Always commit pnpm-lock.yaml to ensure reproducible builds. The lockfile is consumed by corepack pnpm install --frozen-lockfile in CI and Cloudflare Pages.
The site deploys to Cloudflare Pages from the dist/ folder:
pnpm run buildSecurity considerations:
- Build output (
docs/) contains only public content - No
.envfiles in build output - No source maps with sensitive paths
- HTTPS enforced by Cloudflare
If you need secrets during build (e.g., fetching from a CMS):
# In CI/CD, set environment variables
PRIVATE_CMS_TOKEN=xxx pnpm run buildThe secret is used at build time but not included in output.
Cloudflare Pages allows custom headers. For enhanced security, consider:
<!-- src/components/BaseHead.astro -->
<meta http-equiv="X-Content-Type-Options" content="nosniff" />
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN" />If moving to a host with header control (Vercel, Netlify):
# Example _headers file
/*
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
- No secrets in code
- No
.envfiles committed - External links have
rel="noopener noreferrer" - No unnecessary data exposed
-
npm auditshows no critical vulnerabilities - Build output contains only public content
- Environment variables properly configured
- Dependencies are up to date
- Audit dependencies monthly
- Review API routes for data exposure
- Check for new security best practices
- Update packages regularly
If a secret is accidentally committed:
- Rotate immediately - Generate new credentials
- Remove from history - Use
git filter-branchor BFG Repo-Cleaner - Audit usage - Check if secret was used maliciously
- Update documentation - Prevent recurrence