create-clientkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/THIRD-PARTY.md +61 -0
- package/bin/cli.js +52 -0
- package/dist/cli.js +2556 -0
- package/package.json +84 -0
- package/templates/astro-tailwind/base/README.md +131 -0
- package/templates/astro-tailwind/base/_gitattributes +5 -0
- package/templates/astro-tailwind/base/_gitignore +23 -0
- package/templates/astro-tailwind/base/_package.json +28 -0
- package/templates/astro-tailwind/base/astro.config.mjs +32 -0
- package/templates/astro-tailwind/base/public/favicon.svg +5 -0
- package/templates/astro-tailwind/base/src/components/Brand.astro +89 -0
- package/templates/astro-tailwind/base/src/components/Footer.astro +88 -0
- package/templates/astro-tailwind/base/src/components/Header.astro +104 -0
- package/templates/astro-tailwind/base/src/components/LaunchNotice.astro +156 -0
- package/templates/astro-tailwind/base/src/components/Seo.astro +73 -0
- package/templates/astro-tailwind/base/src/components/SocialLinks.astro +75 -0
- package/templates/astro-tailwind/base/src/components/StructuredData.astro +39 -0
- package/templates/astro-tailwind/base/src/config/site.config.ts +141 -0
- package/templates/astro-tailwind/base/src/layouts/BaseLayout.astro +121 -0
- package/templates/astro-tailwind/base/src/lib/seo.ts +87 -0
- package/templates/astro-tailwind/base/src/pages/404.astro +147 -0
- package/templates/astro-tailwind/base/src/pages/index.astro +31 -0
- package/templates/astro-tailwind/base/src/pages/robots.txt.ts +32 -0
- package/templates/astro-tailwind/base/src/styles/global.css +330 -0
- package/templates/astro-tailwind/base/tsconfig.json +11 -0
- package/templates/astro-tailwind/modes/coming-soon/_package.json +3 -0
- package/templates/astro-tailwind/modes/coming-soon/src/pages/index.astro +149 -0
- package/templates/astro-tailwind/modes/full/_package.json +3 -0
- package/templates/astro-tailwind/modes/full/src/pages/index.astro +237 -0
- package/templates/astro-tailwind/template.json +23 -0
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-clientkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create the boring foundation of your next client website in seconds.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"scaffold",
|
|
8
|
+
"generator",
|
|
9
|
+
"client-website",
|
|
10
|
+
"freelance",
|
|
11
|
+
"agency",
|
|
12
|
+
"astro",
|
|
13
|
+
"starter"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Joseph K Anoj",
|
|
18
|
+
"email": "josephkanoj@gmail.com",
|
|
19
|
+
"url": "https://github.com/JosuK22"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20.19"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"create-clientkit": "bin/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"dist",
|
|
31
|
+
"templates",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"THIRD-PARTY.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"lint": "eslint .",
|
|
40
|
+
"lint:fix": "eslint . --fix",
|
|
41
|
+
"format": "prettier --write .",
|
|
42
|
+
"format:check": "prettier --check .",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"third-party": "node scripts/generate-third-party.mjs",
|
|
46
|
+
"third-party:check": "node scripts/generate-third-party.mjs --check",
|
|
47
|
+
"check:package": "node scripts/check-package.mjs",
|
|
48
|
+
"smoke": "node scripts/smoke.mjs",
|
|
49
|
+
"smoke:quick": "node scripts/smoke.mjs --quick",
|
|
50
|
+
"smoke:audit": "node scripts/smoke.mjs --audit",
|
|
51
|
+
"audit": "node scripts/audit-site.mjs",
|
|
52
|
+
"preflight": "node scripts/preflight.mjs",
|
|
53
|
+
"verify": "npm run typecheck && npm run lint && npm run format:check && npm run test && npm run build",
|
|
54
|
+
"prepublishOnly": "npm run preflight",
|
|
55
|
+
"cancel-check": "node scripts/cancel-check.mjs",
|
|
56
|
+
"drift": "node scripts/drift-probe.mjs",
|
|
57
|
+
"drift:report": "node scripts/drift-probe.mjs --report"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@clack/prompts": "^0.11.0",
|
|
61
|
+
"@eslint/js": "^10.0.1",
|
|
62
|
+
"@types/node": "^22.15.0",
|
|
63
|
+
"axe-core": "^4.13.0",
|
|
64
|
+
"eslint": "^10.10.0",
|
|
65
|
+
"globals": "^17.12.0",
|
|
66
|
+
"lighthouse": "^13.4.1",
|
|
67
|
+
"picocolors": "^1.1.1",
|
|
68
|
+
"prettier": "^3.5.3",
|
|
69
|
+
"prettier-plugin-astro": "^0.14.1",
|
|
70
|
+
"puppeteer": "^25.10.0",
|
|
71
|
+
"tsup": "^8.4.0",
|
|
72
|
+
"typescript": "^5.8.3",
|
|
73
|
+
"typescript-eslint": "^8.70.0",
|
|
74
|
+
"vitest": "^3.1.2"
|
|
75
|
+
},
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "git+https://github.com/JosuK22/create-clientkit.git"
|
|
79
|
+
},
|
|
80
|
+
"bugs": {
|
|
81
|
+
"url": "https://github.com/JosuK22/create-clientkit/issues"
|
|
82
|
+
},
|
|
83
|
+
"homepage": "https://github.com/JosuK22/create-clientkit#readme"
|
|
84
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# {{siteName}}
|
|
2
|
+
|
|
3
|
+
{{description}}
|
|
4
|
+
|
|
5
|
+
Scaffolded with [create-clientkit](https://www.npmjs.com/package/create-clientkit).
|
|
6
|
+
This is your code now - edit anything.
|
|
7
|
+
|
|
8
|
+
## Getting started
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Script | What it does |
|
|
16
|
+
| ----------------- | -------------------------------------- |
|
|
17
|
+
| `npm run dev` | Start the dev server |
|
|
18
|
+
| `npm run check` | Type-check `.astro` files and the site |
|
|
19
|
+
| `npm run build` | Build the production site to `dist/` |
|
|
20
|
+
| `npm run preview` | Preview the production build |
|
|
21
|
+
|
|
22
|
+
## Where to start
|
|
23
|
+
|
|
24
|
+
1. **`src/config/site.config.ts`** - the single source of truth, and the only
|
|
25
|
+
file you need for most client changes:
|
|
26
|
+
- `SITE` - name, description, production URL, locale, author
|
|
27
|
+
- `NAV` - header links (empty means no menu is rendered at all)
|
|
28
|
+
- `SOCIAL` - social profiles (empty means no links are rendered)
|
|
29
|
+
- `CONTACT` - email, phone, location; each is optional
|
|
30
|
+
- `LAUNCH` - optional launch date and countdown
|
|
31
|
+
- `THEME` - `appearance` (`system`/`light`/`dark`) and a brand `accent` hex
|
|
32
|
+
- `SEO` - social preview `image`, `twitterCard`, and `noindex`
|
|
33
|
+
|
|
34
|
+
An empty string or empty array means "not set", and the UI omits that piece
|
|
35
|
+
rather than showing a placeholder. Nothing is ever invented for you.
|
|
36
|
+
|
|
37
|
+
2. **`src/pages/index.astro`** - the home page.
|
|
38
|
+
3. **`src/pages/404.astro`** - the custom not-found page.
|
|
39
|
+
4. **`src/layouts/BaseLayout.astro`** - the shared shell: `<head>`, skip link,
|
|
40
|
+
header, footer, and the `measure` prop that sets the page column width.
|
|
41
|
+
5. **`src/components/`** - `Header`, `Footer`, `Brand`, `SocialLinks`,
|
|
42
|
+
`LaunchNotice`.
|
|
43
|
+
6. **`src/styles/global.css`** - the design system. Tailwind v4 is configured in
|
|
44
|
+
CSS via `@theme`; there is no `tailwind.config.js`. Colours, type scale,
|
|
45
|
+
radii, shadows and spacing all live here, and light/dark are one block of
|
|
46
|
+
custom properties rather than duplicated components.
|
|
47
|
+
7. **`public/favicon.svg`** - replace with the client's mark.
|
|
48
|
+
|
|
49
|
+
## SEO
|
|
50
|
+
|
|
51
|
+
Metadata is generated into the static HTML at build time - nothing depends on
|
|
52
|
+
JavaScript, so crawlers and social platforms get the full picture from the
|
|
53
|
+
source.
|
|
54
|
+
|
|
55
|
+
`src/components/Seo.astro` is the single place that emits `<title>`,
|
|
56
|
+
description, canonical, robots, Open Graph and Twitter tags, and
|
|
57
|
+
`src/layouts/BaseLayout.astro` renders it for every page. A page only passes
|
|
58
|
+
what it needs to override:
|
|
59
|
+
|
|
60
|
+
```astro
|
|
61
|
+
<BaseLayout title="About" description="Something specific to this page." />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Everything derives from `SITE`.** You never repeat the site name,
|
|
65
|
+
description or URL.
|
|
66
|
+
|
|
67
|
+
### Before you have a domain
|
|
68
|
+
|
|
69
|
+
`SITE.url` starts empty and that is a supported state, not a broken one. While
|
|
70
|
+
it is empty the site omits every absolute tag - canonical, `og:url`, the
|
|
71
|
+
sitemap and the sitemap line in `robots.txt` - rather than pointing them at a
|
|
72
|
+
domain nobody owns yet. Fill `SITE.url` in and they all appear. Only the
|
|
73
|
+
origin is used; a subpath deployment also needs Astro's `base` option.
|
|
74
|
+
|
|
75
|
+
### Indexing
|
|
76
|
+
|
|
77
|
+
`SEO.noindex` controls it, and it is `false` by default so a launched site is
|
|
78
|
+
findable. Set it to `true` while a holding page is up if you would rather it
|
|
79
|
+
stayed out of search - and remember to switch it back at launch.
|
|
80
|
+
|
|
81
|
+
| `SITE.url` | `SEO.noindex` | Result |
|
|
82
|
+
| ---------- | ------------- | ------------------------------------------------------------ |
|
|
83
|
+
| set | `false` | Canonical, `og:url`, sitemap, `Sitemap:` line in robots.txt |
|
|
84
|
+
| set | `true` | `noindex, nofollow`, no canonical, no sitemap, `Disallow: /` |
|
|
85
|
+
| empty | `false` | No absolute tags, no sitemap, permissive robots.txt |
|
|
86
|
+
| empty | `true` | No absolute tags, no sitemap, `Disallow: /` |
|
|
87
|
+
|
|
88
|
+
The 404 page is always `noindex, nofollow` and carries no structured data,
|
|
89
|
+
whatever the site setting says.
|
|
90
|
+
|
|
91
|
+
### Social preview image
|
|
92
|
+
|
|
93
|
+
`SEO.image` is empty by default, so no `og:image` is emitted at all - better
|
|
94
|
+
than pointing Facebook and X at a file that isn't there. Drop a 1200x630 PNG
|
|
95
|
+
into `public/` and set `image: '/og.png'`.
|
|
96
|
+
|
|
97
|
+
### Structured data
|
|
98
|
+
|
|
99
|
+
`src/components/StructuredData.astro` emits a schema.org `Organization` block
|
|
100
|
+
built only from values you have actually configured. No logo, address, rating
|
|
101
|
+
or review is invented - fields you leave empty simply do not appear.
|
|
102
|
+
|
|
103
|
+
## Design notes
|
|
104
|
+
|
|
105
|
+
- **Zero client-side JavaScript by default.** The only script in the project is
|
|
106
|
+
a ~20-line countdown, and it ships only when you set a launch date. Without
|
|
107
|
+
JavaScript the launch date still renders; only the ticking numbers are lost.
|
|
108
|
+
- **Dark mode needs no JavaScript.** It follows the visitor's system preference,
|
|
109
|
+
or you can force one via `THEME.appearance`.
|
|
110
|
+
- **Set `THEME.accent`** to the client's brand hex and buttons, focus rings and
|
|
111
|
+
the background wash all follow. Readable text on the accent is chosen for you.
|
|
112
|
+
- **Motion is opt-out-aware**: entrance animation only runs under
|
|
113
|
+
`prefers-reduced-motion: no-preference`, so content is never hidden behind an
|
|
114
|
+
animation that may not play.
|
|
115
|
+
|
|
116
|
+
## Stack
|
|
117
|
+
|
|
118
|
+
- [Astro](https://astro.build) 7 - static output, zero client JS by default
|
|
119
|
+
- [Tailwind CSS](https://tailwindcss.com) 4 - loaded through `@tailwindcss/vite`
|
|
120
|
+
- TypeScript in strict mode
|
|
121
|
+
|
|
122
|
+
Requires Node.js 22.12 or newer.
|
|
123
|
+
|
|
124
|
+
## Notes
|
|
125
|
+
|
|
126
|
+
`.client-site.json` records how this project was generated. Leave it in place -
|
|
127
|
+
future `create-clientkit` tooling reads it. It contains no secrets.
|
|
128
|
+
|
|
129
|
+
This project is private and unlicensed by default (`"private": true`,
|
|
130
|
+
`"license": "UNLICENSED"`), because client work is normally proprietary. Change
|
|
131
|
+
that in `package.json` if the site is meant to be open source.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# build output
|
|
2
|
+
dist/
|
|
3
|
+
.astro/
|
|
4
|
+
|
|
5
|
+
# dependencies
|
|
6
|
+
node_modules/
|
|
7
|
+
|
|
8
|
+
# logs
|
|
9
|
+
npm-debug.log*
|
|
10
|
+
yarn-debug.log*
|
|
11
|
+
yarn-error.log*
|
|
12
|
+
pnpm-debug.log*
|
|
13
|
+
|
|
14
|
+
# environment
|
|
15
|
+
.env
|
|
16
|
+
.env.production
|
|
17
|
+
|
|
18
|
+
# editors and OS
|
|
19
|
+
.vscode/*
|
|
20
|
+
!.vscode/extensions.json
|
|
21
|
+
.idea/
|
|
22
|
+
.DS_Store
|
|
23
|
+
Thumbs.db
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.12.0"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["astro", "client-site"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "astro dev",
|
|
13
|
+
"build": "astro build",
|
|
14
|
+
"preview": "astro preview",
|
|
15
|
+
"check": "astro check",
|
|
16
|
+
"astro": "astro"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@astrojs/sitemap": "3.7.4",
|
|
20
|
+
"astro": "7.3.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@astrojs/check": "0.9.10",
|
|
24
|
+
"@tailwindcss/vite": "4.3.3",
|
|
25
|
+
"tailwindcss": "4.3.3",
|
|
26
|
+
"typescript": "5.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig } from 'astro/config';
|
|
2
|
+
import sitemap from '@astrojs/sitemap';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
|
|
5
|
+
import { SEO, SITE } from './src/config/site.config.ts';
|
|
6
|
+
import { siteOrigin } from './src/lib/seo.ts';
|
|
7
|
+
|
|
8
|
+
// '' when no production URL is configured. Astro rejects an empty or malformed
|
|
9
|
+
// `site`, and an absolute sitemap cannot exist without a real domain, so both
|
|
10
|
+
// are switched on by the same condition rather than guessing a URL.
|
|
11
|
+
const origin = siteOrigin(SITE.url);
|
|
12
|
+
|
|
13
|
+
// A site asking not to be indexed should not also publish a sitemap listing
|
|
14
|
+
// every page it wants ignored.
|
|
15
|
+
const wantsSitemap = origin !== '' && !SEO.noindex;
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
...(origin ? { site: origin } : {}),
|
|
19
|
+
integrations: [
|
|
20
|
+
...(wantsSitemap
|
|
21
|
+
? [
|
|
22
|
+
sitemap({
|
|
23
|
+
// 404 is a status page, not content to be crawled.
|
|
24
|
+
filter: (page) => !page.includes('/404'),
|
|
25
|
+
}),
|
|
26
|
+
]
|
|
27
|
+
: []),
|
|
28
|
+
],
|
|
29
|
+
vite: {
|
|
30
|
+
plugins: [tailwindcss()],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Site icon">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#0f172a" />
|
|
3
|
+
<path d="M20 40V24h6.4c4.8 0 7.6 2.7 7.6 7s-2.8 7-7.6 7H20Zm4.4-3.4h1.9c2.6 0 4-1.4 4-3.6s-1.4-3.6-4-3.6h-1.9v7.2Z" fill="#f8fafc" />
|
|
4
|
+
<circle cx="42" cy="38" r="3" fill="#38bdf8" />
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SITE } from '../config/site.config.ts';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** 'sm' for header and footer, 'lg' for a page's own brand lockup. */
|
|
6
|
+
size?: 'sm' | 'lg';
|
|
7
|
+
/** Wrap in a link home. Off when the brand already sits on the home page. */
|
|
8
|
+
href?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { size = 'sm', href } = Astro.props;
|
|
12
|
+
|
|
13
|
+
// A compact mark derived from the site name: up to two initials. It is a
|
|
14
|
+
// neutral stand-in until the client's real logo is dropped in.
|
|
15
|
+
const initials = SITE.name
|
|
16
|
+
.split(/\s+/)
|
|
17
|
+
.filter((word) => /[a-z0-9]/i.test(word))
|
|
18
|
+
.slice(0, 2)
|
|
19
|
+
.map((word) => word[0]?.toUpperCase() ?? '')
|
|
20
|
+
.join('');
|
|
21
|
+
|
|
22
|
+
const Tag = href ? 'a' : 'div';
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<Tag class:list={['brand', size]} href={href} aria-label={href ? SITE.name : undefined}>
|
|
26
|
+
<span class="mark" aria-hidden="true">{initials}</span>
|
|
27
|
+
<span class="name">{SITE.name}</span>
|
|
28
|
+
</Tag>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.brand {
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 0.625rem;
|
|
35
|
+
color: var(--color-foreground);
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.mark {
|
|
41
|
+
display: grid;
|
|
42
|
+
place-items: center;
|
|
43
|
+
flex: none;
|
|
44
|
+
border: 1px solid var(--color-border-strong);
|
|
45
|
+
border-radius: var(--radius-md);
|
|
46
|
+
background-color: var(--color-surface);
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
letter-spacing: -0.02em;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.name {
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
letter-spacing: -0.015em;
|
|
54
|
+
/* Long names truncate in the header rather than pushing the layout apart. */
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sm .mark {
|
|
61
|
+
width: 1.875rem;
|
|
62
|
+
height: 1.875rem;
|
|
63
|
+
font-size: 0.75rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.sm .name {
|
|
67
|
+
font-size: 0.9375rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.lg {
|
|
71
|
+
gap: 0.875rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.lg .mark {
|
|
75
|
+
width: 2.75rem;
|
|
76
|
+
height: 2.75rem;
|
|
77
|
+
font-size: 1rem;
|
|
78
|
+
border-radius: var(--radius-lg);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.lg .name {
|
|
82
|
+
font-size: 1.0625rem;
|
|
83
|
+
white-space: normal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
a.brand:hover .mark {
|
|
87
|
+
border-color: var(--color-foreground);
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
import SocialLinks from './SocialLinks.astro';
|
|
3
|
+
import { CONTACT, SITE, SOCIAL } from '../config/site.config.ts';
|
|
4
|
+
|
|
5
|
+
// Computed at build time rather than frozen in at scaffold time, so the notice
|
|
6
|
+
// stays correct for as long as the site is rebuilt.
|
|
7
|
+
const year = new Date().getFullYear();
|
|
8
|
+
|
|
9
|
+
const hasSocial = SOCIAL.some((link) => link.label !== '' && link.href !== '');
|
|
10
|
+
const hasContact = CONTACT.email !== '' || CONTACT.phone !== '';
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<footer class="site-footer">
|
|
14
|
+
<div class="container-page inner">
|
|
15
|
+
<p class="copyright">
|
|
16
|
+
© {year}
|
|
17
|
+
{SITE.name}
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
(hasContact || hasSocial) && (
|
|
22
|
+
<div class="links">
|
|
23
|
+
{CONTACT.email !== '' && (
|
|
24
|
+
<a href={`mailto:${CONTACT.email}`} class="contact">
|
|
25
|
+
{CONTACT.email}
|
|
26
|
+
</a>
|
|
27
|
+
)}
|
|
28
|
+
{CONTACT.phone !== '' && (
|
|
29
|
+
<a href={`tel:${CONTACT.phone.replace(/[^+\d]/g, '')}`} class="contact">
|
|
30
|
+
{CONTACT.phone}
|
|
31
|
+
</a>
|
|
32
|
+
)}
|
|
33
|
+
{hasSocial && <SocialLinks label="Social links in the footer" />}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
</div>
|
|
38
|
+
</footer>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.site-footer {
|
|
42
|
+
margin-top: auto;
|
|
43
|
+
border-top: 1px solid var(--color-border);
|
|
44
|
+
padding-block: 2rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.inner {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
gap: 0.75rem 2rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.copyright {
|
|
56
|
+
margin: 0;
|
|
57
|
+
color: var(--color-muted);
|
|
58
|
+
font-size: var(--text-small);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.links {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-wrap: wrap;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 0.25rem 1.25rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.contact {
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
min-height: 2.25rem;
|
|
72
|
+
color: var(--color-muted);
|
|
73
|
+
font-size: var(--text-small);
|
|
74
|
+
text-decoration: none;
|
|
75
|
+
transition: color 160ms ease;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.contact:hover {
|
|
79
|
+
color: var(--color-foreground);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@media (max-width: 40rem) {
|
|
83
|
+
.inner {
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
align-items: flex-start;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Brand from './Brand.astro';
|
|
3
|
+
import type { NavItem } from '../config/site.config.ts';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
items?: NavItem[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { items = [] } = Astro.props;
|
|
10
|
+
const currentPath = Astro.url.pathname;
|
|
11
|
+
const isHome = currentPath === '/' || currentPath === '';
|
|
12
|
+
|
|
13
|
+
// No links means no menu. A header with an invented nav is worse than none.
|
|
14
|
+
const hasNav = items.length > 0;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<header class="site-header">
|
|
18
|
+
<div class="container-page inner">
|
|
19
|
+
<Brand size="sm" href={isHome ? undefined : '/'} />
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
hasNav && (
|
|
23
|
+
<nav aria-label="Primary">
|
|
24
|
+
<ul>
|
|
25
|
+
{items.map((item) => (
|
|
26
|
+
<li>
|
|
27
|
+
<a href={item.href} aria-current={item.href === currentPath ? 'page' : undefined}>
|
|
28
|
+
{item.label}
|
|
29
|
+
</a>
|
|
30
|
+
</li>
|
|
31
|
+
))}
|
|
32
|
+
</ul>
|
|
33
|
+
</nav>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
</div>
|
|
37
|
+
</header>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.site-header {
|
|
41
|
+
border-bottom: 1px solid var(--color-border);
|
|
42
|
+
background-color: color-mix(in oklab, var(--color-background) 88%, transparent);
|
|
43
|
+
backdrop-filter: saturate(1.4) blur(8px);
|
|
44
|
+
position: sticky;
|
|
45
|
+
top: 0;
|
|
46
|
+
z-index: 20;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.inner {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
gap: 1rem;
|
|
54
|
+
min-height: 4rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
nav ul {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
justify-content: flex-end;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 0.25rem 1.25rem;
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
list-style: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
nav a {
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
min-height: 2.5rem;
|
|
72
|
+
padding: 0.25rem 0;
|
|
73
|
+
color: var(--color-muted);
|
|
74
|
+
font-size: var(--text-small);
|
|
75
|
+
font-weight: 500;
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
transition: color 160ms ease;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
nav a:hover,
|
|
81
|
+
nav a[aria-current='page'] {
|
|
82
|
+
color: var(--color-foreground);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
nav a[aria-current='page'] {
|
|
86
|
+
box-shadow: inset 0 -1px 0 0 var(--color-accent);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Below 480px the brand keeps the top line to itself and the links wrap
|
|
90
|
+
underneath, which reads better than squeezing both onto one row. */
|
|
91
|
+
@media (max-width: 30rem) {
|
|
92
|
+
.inner {
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
align-items: flex-start;
|
|
95
|
+
gap: 0.25rem;
|
|
96
|
+
padding-block: 0.75rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
nav ul {
|
|
100
|
+
justify-content: flex-start;
|
|
101
|
+
gap: 0.25rem 1rem;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
</style>
|