Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: '1.1.36'

- name: Install dependencies
run: bun install

- name: Run type checking
run: bun run typecheck

- name: Run tests
run: bun test

- name: Build site
run: bun run build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
if: success()
with:
name: dist
path: dist/
retention-days: 7
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"start": "astro preview",
"typecheck": "tsc --noEmit",
"test": "bun test",
"test:coverage": "vitest run --coverage",
"build:favicon": "bash scripts/build-favicons.sh",
"astro": "astro"
},
Expand All @@ -26,7 +27,6 @@
"@astrojs/tailwind": "^6.0.2",
"@heroicons/react": "^2.2.0",
"@radix-ui/react-tabs": "^1.1.12",
"add": "^2.0.6",
"astro": "^5.11.0",
"autoprefixer": "^10.4.21",
"bun": "^1.2.18",
Expand All @@ -39,7 +39,7 @@
"sharp": "^0.33.5",
"shiki": "^3.7.0",
"tailwind-merge": "^2.6.0",
"tailwindcss": "3.3.6"
"tailwindcss": "^3.4.17"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
Expand All @@ -60,7 +60,8 @@
"postcss": "^8.5.6",
"puppeteer": "^24.12.1",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4"
},
"engines": {
"node": "22.12.0",
Expand Down
66 changes: 66 additions & 0 deletions src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component, type ReactNode } from "react";

interface Props {
children: ReactNode;
fallback?: ReactNode;
}

interface State {
hasError: boolean;
error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
console.error("ErrorBoundary caught an error:", error, errorInfo);
}

render(): ReactNode {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-red-600 dark:text-red-400 mb-4">
Something went wrong
</h2>
<p className="text-gray-700 dark:text-gray-300 mb-4">
We're sorry, but something unexpected happened. Please try refreshing the page.
</p>
{this.state.error && (
<details className="mt-4">
<summary className="cursor-pointer text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200">
Error details
</summary>
<pre className="mt-2 p-4 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-auto">
{this.state.error.message}
</pre>
</details>
)}
<button
type="button"
onClick={() => window.location.reload()}
className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition-colors"
>
Reload page
</button>
</div>
</div>
);
}

return this.props.children;
}
}
8 changes: 7 additions & 1 deletion src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const { title = "Oliver Steele", description = "Making, teaching, writing, playi
<meta name="author" content="Oliver Steele" />
</head>
<body class="flex flex-col min-h-full">
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[200] focus:bg-blue-600 focus:text-white focus:px-4 focus:py-2 focus:rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Skip to main content
</a>
<header
class="fixed top-0 w-full bg-white/80 dark:bg-black/80 backdrop-blur-sm z-[100] border-b border-gray-200 dark:border-gray-800"
>
Expand Down Expand Up @@ -73,7 +79,7 @@ const { title = "Oliver Steele", description = "Making, teaching, writing, playi
</nav>
</header>

<main class="pt-16">
<main id="main-content" class="pt-16">
<slot />
</main>

Expand Down
29 changes: 29 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();

// Add Content-Security-Policy header
// Note: Adjust these directives based on your specific needs
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // unsafe-inline/eval needed for Astro's client-side scripts
"style-src 'self' 'unsafe-inline'", // unsafe-inline needed for Tailwind and inline styles
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; ");

response.headers.set("Content-Security-Policy", csp);

// Add other security headers
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set("Permissions-Policy", "geolocation=(), microphone=(), camera=()");

return response;
});
12 changes: 12 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,17 @@ export default defineConfig({
},
// Increase timeout for integration tests but not too much
testTimeout: 30000,
// Coverage configuration
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/**',
'dist/**',
'**/*.test.ts',
'**/*.config.*',
'**/types/**',
],
},
},
});
Loading