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.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/THIRD-PARTY.md +61 -0
  4. package/bin/cli.js +52 -0
  5. package/dist/cli.js +2556 -0
  6. package/package.json +84 -0
  7. package/templates/astro-tailwind/base/README.md +131 -0
  8. package/templates/astro-tailwind/base/_gitattributes +5 -0
  9. package/templates/astro-tailwind/base/_gitignore +23 -0
  10. package/templates/astro-tailwind/base/_package.json +28 -0
  11. package/templates/astro-tailwind/base/astro.config.mjs +32 -0
  12. package/templates/astro-tailwind/base/public/favicon.svg +5 -0
  13. package/templates/astro-tailwind/base/src/components/Brand.astro +89 -0
  14. package/templates/astro-tailwind/base/src/components/Footer.astro +88 -0
  15. package/templates/astro-tailwind/base/src/components/Header.astro +104 -0
  16. package/templates/astro-tailwind/base/src/components/LaunchNotice.astro +156 -0
  17. package/templates/astro-tailwind/base/src/components/Seo.astro +73 -0
  18. package/templates/astro-tailwind/base/src/components/SocialLinks.astro +75 -0
  19. package/templates/astro-tailwind/base/src/components/StructuredData.astro +39 -0
  20. package/templates/astro-tailwind/base/src/config/site.config.ts +141 -0
  21. package/templates/astro-tailwind/base/src/layouts/BaseLayout.astro +121 -0
  22. package/templates/astro-tailwind/base/src/lib/seo.ts +87 -0
  23. package/templates/astro-tailwind/base/src/pages/404.astro +147 -0
  24. package/templates/astro-tailwind/base/src/pages/index.astro +31 -0
  25. package/templates/astro-tailwind/base/src/pages/robots.txt.ts +32 -0
  26. package/templates/astro-tailwind/base/src/styles/global.css +330 -0
  27. package/templates/astro-tailwind/base/tsconfig.json +11 -0
  28. package/templates/astro-tailwind/modes/coming-soon/_package.json +3 -0
  29. package/templates/astro-tailwind/modes/coming-soon/src/pages/index.astro +149 -0
  30. package/templates/astro-tailwind/modes/full/_package.json +3 -0
  31. package/templates/astro-tailwind/modes/full/src/pages/index.astro +237 -0
  32. 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,5 @@
1
+ * text=auto eol=lf
2
+ *.png binary
3
+ *.jpg binary
4
+ *.ico binary
5
+ *.woff2 binary
@@ -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
+ &copy; {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>