feat(site): 🚀 add Astro marketing/content site
Standalone Astro 6 site in /site (decoupled from the /web app monorepo): landing page, /blog with one post (Content Layer), and terms/privacy/cookie legal pages. Pure Astro, server-rendered to static HTML. - Static by default via @astrojs/vercel adapter; any route can opt into SSR with `export const prerender = false` (deploys as a Vercel Function). - SPA-style navigation with <ClientRouter />, transition:persist on the header (no icon flash), and viewport prefetch — ~6 KB gzip JS, no React. - Deploy on Vercel by setting Root Directory to 'site' (no vercel.json needed). See site/README.md for dev and deploy instructions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d677ffca51
commit
d55a8a0afa
20 changed files with 6154 additions and 0 deletions
16
site/.gitignore
vendored
Normal file
16
site/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# build output
|
||||
dist/
|
||||
.vercel/
|
||||
|
||||
# generated content collection types & cache
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# environment
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
75
site/README.md
Normal file
75
site/README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# site
|
||||
|
||||
Public-facing marketing/content site for the KiCad WebAssembly project: landing
|
||||
page, blog, and legal pages. Built with **Astro 6**.
|
||||
|
||||
This is a **standalone** project — it is intentionally decoupled from the `/web`
|
||||
app monorepo (which is the product itself: React frontend + Fastify backend).
|
||||
It uses **npm** (not the monorepo's pnpm) and has its own `package-lock.json`.
|
||||
|
||||
## What it ships
|
||||
|
||||
- Static by default: every page is prerendered to HTML and ships **zero client
|
||||
JavaScript**. A visitor downloads HTML + CSS only — no React/JS bundle.
|
||||
- SSR-capable: the Vercel adapter is wired in, so any individual route can be
|
||||
switched to per-request server rendering without ripping anything out (see
|
||||
below).
|
||||
|
||||
## Routes
|
||||
|
||||
| Route | Source |
|
||||
| --------------------- | --------------------------------------- |
|
||||
| `/` | `src/pages/index.astro` |
|
||||
| `/blog` | `src/pages/blog/index.astro` |
|
||||
| `/blog/<id>` | `src/pages/blog/[slug].astro` |
|
||||
| `/terms` | `src/pages/terms.astro` |
|
||||
| `/privacy` | `src/pages/privacy.astro` |
|
||||
| `/cookies` | `src/pages/cookies.astro` |
|
||||
|
||||
Blog posts are Markdown files in `src/content/blog/`, validated by the schema in
|
||||
`src/content.config.ts`. Add a post by dropping a new `.md` file there with
|
||||
`title`, `description`, and `pubDate` frontmatter.
|
||||
|
||||
## Local development
|
||||
|
||||
Requires **Node ≥ 22.12** (Astro 6 requirement).
|
||||
|
||||
```bash
|
||||
cd site
|
||||
npm install
|
||||
npm run dev # http://localhost:4321
|
||||
npm run build # outputs to dist/ (+ .vercel/output for the adapter)
|
||||
npm run preview # serve the production build locally
|
||||
```
|
||||
|
||||
## SSR per route
|
||||
|
||||
Pages are static by default. To render a specific page or endpoint on demand
|
||||
(per request, as a Vercel Function), add this to its frontmatter:
|
||||
|
||||
```astro
|
||||
---
|
||||
export const prerender = false;
|
||||
---
|
||||
```
|
||||
|
||||
That's the only change needed — the `@astrojs/vercel` adapter in
|
||||
`astro.config.mjs` already provides the server runtime. The rest of the site
|
||||
stays static.
|
||||
|
||||
## Deploying to Vercel
|
||||
|
||||
The `@astrojs/vercel` adapter emits the Vercel Build Output API format, so **no
|
||||
`vercel.json` is required**.
|
||||
|
||||
1. Push this repo to GitHub.
|
||||
2. Vercel dashboard → **New Project** → import this repo.
|
||||
3. Click **Edit** next to **Root Directory** and set it to **`site`**. This is
|
||||
the standard way to deploy a project that lives in a subdirectory.
|
||||
4. Vercel auto-detects the **Astro** framework preset and the package manager
|
||||
from the lockfile. Leave the build/install commands at their defaults.
|
||||
5. Deploy. Static pages are served from the CDN; any route with
|
||||
`prerender = false` is deployed as a Vercel Function automatically.
|
||||
|
||||
Note: do **not** use `vercel.json` for URL rewrites with Astro — use Astro's
|
||||
`redirects` option in `astro.config.mjs` instead.
|
||||
14
site/astro.config.mjs
Normal file
14
site/astro.config.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import vercel from '@astrojs/vercel';
|
||||
|
||||
// Static by default (every page prerenders to HTML, zero client JS).
|
||||
// The Vercel adapter is wired in so any single route can opt into
|
||||
// per-request SSR later with `export const prerender = false;`.
|
||||
// See README.md ("SSR per route") for how.
|
||||
export default defineConfig({
|
||||
output: 'static',
|
||||
adapter: vercel(),
|
||||
// Prefetch linked pages so SPA-style navigation feels instant.
|
||||
prefetch: { prefetchAll: true, defaultStrategy: 'viewport' },
|
||||
});
|
||||
5526
site/package-lock.json
generated
Normal file
5526
site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
site/package.json
Normal file
19
site/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "site",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^6.4.4",
|
||||
"@astrojs/vercel": "^10.0.8"
|
||||
}
|
||||
}
|
||||
4
site/public/favicon.svg
Normal file
4
site/public/favicon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="6" fill="#1d4ed8" />
|
||||
<path d="M9 23V9h2.6v6.2L17 9h3.2l-5.6 6.4L20.6 23h-3.3l-4.3-5.8-1.4 1.6V23H9z" fill="#fff" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 221 B |
2
site/public/robots.txt
Normal file
2
site/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
14
site/src/components/Footer.astro
Normal file
14
site/src/components/Footer.astro
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<span>© {year} KiCad WebAssembly — placeholder.</span>
|
||||
<nav aria-label="Legal">
|
||||
<a href="/terms">Terms & Conditions</a>
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
<a href="/cookies">Cookie Policy</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
18
site/src/components/Header.astro
Normal file
18
site/src/components/Header.astro
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
const path = Astro.url.pathname;
|
||||
const isActive = (href: string) =>
|
||||
href === '/' ? path === '/' : path.startsWith(href);
|
||||
---
|
||||
|
||||
<header class="site-header" transition:persist>
|
||||
<div class="container">
|
||||
<a class="brand" href="/">
|
||||
<img src="/favicon.svg" alt="" width="24" height="24" />
|
||||
<span>KiCad in the browser</span>
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="/" aria-current={isActive('/') ? 'page' : undefined}>Home</a>
|
||||
<a href="/blog" aria-current={isActive('/blog') ? 'page' : undefined}>Blog</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
15
site/src/content.config.ts
Normal file
15
site/src/content.config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { defineCollection, z } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
|
||||
// Content Layer API (Astro 5+). Markdown files live in src/content/blog/.
|
||||
// NOTE: this config file is src/content.config.ts — NOT src/content/config.ts.
|
||||
const blog = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
pubDate: z.coerce.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
||||
19
site/src/content/blog/hello-world.md
Normal file
19
site/src/content/blog/hello-world.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: "Hello World"
|
||||
description: "First placeholder post for the KiCad WebAssembly blog."
|
||||
pubDate: 2026-06-07
|
||||
---
|
||||
|
||||
This is a placeholder blog post. Replace this body with real content.
|
||||
|
||||
## A heading
|
||||
|
||||
Markdown content is rendered to static HTML at build time, so this page ships
|
||||
**zero JavaScript** to the browser.
|
||||
|
||||
- Bullet one
|
||||
- Bullet two
|
||||
- Bullet three
|
||||
|
||||
To add another post, drop a new `.md` file in `src/content/blog/` with the same
|
||||
frontmatter fields (`title`, `description`, `pubDate`).
|
||||
40
site/src/layouts/BaseLayout.astro
Normal file
40
site/src/layouts/BaseLayout.astro
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title, description = 'KiCad in the browser — WebAssembly port.' } =
|
||||
Astro.props;
|
||||
const fullTitle =
|
||||
title === 'KiCad in the browser'
|
||||
? title
|
||||
: `${title} — KiCad in the browser`;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="description" content={description} />
|
||||
<title>{fullTitle}</title>
|
||||
{/* SPA-style navigation: swaps the page without a full reload. */}
|
||||
<ClientRouter />
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<div class="container">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
39
site/src/pages/blog/[slug].astro
Normal file
39
site/src/pages/blog/[slug].astro
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import type { GetStaticPaths } from 'astro';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const posts = await getCollection('blog');
|
||||
// post.id is the URL-safe identifier for glob-loaded collections
|
||||
// (entry.slug was removed in Astro 5).
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: { post },
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout title={post.data.title} description={post.data.description}>
|
||||
<article class="prose">
|
||||
<p class="muted">
|
||||
<a href="/blog">← Back to blog</a>
|
||||
</p>
|
||||
<h1>{post.data.title}</h1>
|
||||
<p class="muted">
|
||||
<time datetime={post.data.pubDate.toISOString()}>
|
||||
{dateFmt.format(post.data.pubDate)}
|
||||
</time>
|
||||
</p>
|
||||
<Content />
|
||||
</article>
|
||||
</BaseLayout>
|
||||
35
site/src/pages/blog/index.astro
Normal file
35
site/src/pages/blog/index.astro
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const posts = (await getCollection('blog')).sort(
|
||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||
);
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout title="Blog" description="Placeholder blog index.">
|
||||
<h1>Blog</h1>
|
||||
<p class="muted">Placeholder blog index. Posts live in src/content/blog/.</p>
|
||||
|
||||
<ul class="post-list">
|
||||
{
|
||||
posts.map((post) => (
|
||||
<li class="card">
|
||||
<time datetime={post.data.pubDate.toISOString()}>
|
||||
{dateFmt.format(post.data.pubDate)}
|
||||
</time>
|
||||
<h3>
|
||||
<a href={`/blog/${post.id}`}>{post.data.title}</a>
|
||||
</h3>
|
||||
<p class="muted">{post.data.description}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</BaseLayout>
|
||||
25
site/src/pages/cookies.astro
Normal file
25
site/src/pages/cookies.astro
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Cookie Policy" description="Placeholder cookie policy.">
|
||||
<article class="prose">
|
||||
<h1>Cookie Policy</h1>
|
||||
<p class="muted">Last updated: placeholder. This text is a placeholder.</p>
|
||||
|
||||
<h2>1. What are cookies</h2>
|
||||
<p>
|
||||
Placeholder content. Replace with your actual cookie policy before going
|
||||
live.
|
||||
</p>
|
||||
|
||||
<h2>2. Cookies we use</h2>
|
||||
<p>Placeholder content.</p>
|
||||
|
||||
<h2>3. Managing cookies</h2>
|
||||
<p>Placeholder content.</p>
|
||||
|
||||
<h2>4. Contact</h2>
|
||||
<p>Placeholder content.</p>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
36
site/src/pages/index.astro
Normal file
36
site/src/pages/index.astro
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="KiCad in the browser">
|
||||
<section class="hero">
|
||||
<h1>KiCad in the browser</h1>
|
||||
<p class="lead">
|
||||
Placeholder landing page for the KiCad WebAssembly port — run PCB and
|
||||
schematic tools in a browser, no installation required. Copy here is a
|
||||
placeholder and will be replaced.
|
||||
</p>
|
||||
<a class="cta" href="/blog">Read the blog</a>
|
||||
</section>
|
||||
|
||||
<section class="grid" aria-label="Highlights">
|
||||
<div class="card">
|
||||
<h3>Placeholder feature</h3>
|
||||
<p class="muted">
|
||||
Short description of something the project does. Replace with real copy.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Placeholder feature</h3>
|
||||
<p class="muted">
|
||||
Short description of something the project does. Replace with real copy.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Placeholder feature</h3>
|
||||
<p class="muted">
|
||||
Short description of something the project does. Replace with real copy.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
28
site/src/pages/privacy.astro
Normal file
28
site/src/pages/privacy.astro
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Privacy Policy" description="Placeholder privacy policy.">
|
||||
<article class="prose">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="muted">Last updated: placeholder. This text is a placeholder.</p>
|
||||
|
||||
<h2>1. Information we collect</h2>
|
||||
<p>
|
||||
Placeholder content. Replace with your actual privacy policy before going
|
||||
live.
|
||||
</p>
|
||||
|
||||
<h2>2. How we use information</h2>
|
||||
<p>Placeholder content.</p>
|
||||
|
||||
<h2>3. Data retention</h2>
|
||||
<p>Placeholder content.</p>
|
||||
|
||||
<h2>4. Your rights</h2>
|
||||
<p>Placeholder content.</p>
|
||||
|
||||
<h2>5. Contact</h2>
|
||||
<p>Placeholder content.</p>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
25
site/src/pages/terms.astro
Normal file
25
site/src/pages/terms.astro
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Terms & Conditions" description="Placeholder terms and conditions.">
|
||||
<article class="prose">
|
||||
<h1>Terms & Conditions</h1>
|
||||
<p class="muted">Last updated: placeholder. This text is a placeholder.</p>
|
||||
|
||||
<h2>1. Introduction</h2>
|
||||
<p>
|
||||
Placeholder content. Replace with your actual terms and conditions before
|
||||
going live.
|
||||
</p>
|
||||
|
||||
<h2>2. Use of the service</h2>
|
||||
<p>Placeholder content.</p>
|
||||
|
||||
<h2>3. Limitation of liability</h2>
|
||||
<p>Placeholder content.</p>
|
||||
|
||||
<h2>4. Contact</h2>
|
||||
<p>Placeholder content.</p>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
199
site/src/styles/global.css
Normal file
199
site/src/styles/global.css
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
:root {
|
||||
--bg: #0b1020;
|
||||
--bg-soft: #11182e;
|
||||
--fg: #e7ecf5;
|
||||
--fg-muted: #9aa6c0;
|
||||
--accent: #4f7cff;
|
||||
--border: #233052;
|
||||
--max-width: 64rem;
|
||||
--radius: 10px;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
line-height: 1.6;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
line-height: 1.2;
|
||||
margin: 0 0 0.5em;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin-inline: auto;
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding-block: 3rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.site-header .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding-block: 1rem;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
color: var(--fg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
.nav a:hover {
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Hero / landing */
|
||||
.hero {
|
||||
padding-block: 2rem 1rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
}
|
||||
|
||||
.hero p.lead {
|
||||
font-size: 1.2rem;
|
||||
color: var(--fg-muted);
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-block;
|
||||
margin-top: 1.25rem;
|
||||
padding: 0.7rem 1.25rem;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cta:hover {
|
||||
text-decoration: none;
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
/* Feature grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
/* Blog list */
|
||||
.post-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.post-list time {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
/* Prose (legal pages, blog post body) */
|
||||
.prose {
|
||||
max-width: 44rem;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-soft);
|
||||
color: var(--fg-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.site-footer .container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-block: 1.5rem;
|
||||
}
|
||||
|
||||
.site-footer nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
5
site/tsconfig.json
Normal file
5
site/tsconfig.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Loading…
Reference in a new issue