simple-site-generator 0.1.2 → 0.2.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 (57) hide show
  1. package/dist/cli.js +165 -2
  2. package/dist/commands/add-page.js +50 -0
  3. package/dist/commands/build.js +20 -0
  4. package/dist/commands/check.js +53 -0
  5. package/dist/commands/init.js +229 -0
  6. package/dist/commands/verify.js +150 -0
  7. package/dist/pages/page-schema.js +161 -0
  8. package/dist/pages/render-page.js +46 -0
  9. package/dist/paths.js +27 -0
  10. package/dist/types/cli.d.ts +1 -0
  11. package/dist/types/commands/add-page.d.ts +26 -0
  12. package/dist/types/commands/build.d.ts +18 -0
  13. package/dist/types/commands/check.d.ts +20 -0
  14. package/dist/types/commands/init.d.ts +40 -0
  15. package/dist/types/commands/verify.d.ts +47 -0
  16. package/dist/types/pages/page-schema.d.ts +81 -0
  17. package/dist/types/pages/render-page.d.ts +26 -0
  18. package/dist/types/paths.d.ts +16 -0
  19. package/dist/types/verify/budgets.d.ts +114 -0
  20. package/dist/types/verify/class-coverage.d.ts +65 -0
  21. package/dist/types/verify/ignore.d.ts +8 -0
  22. package/dist/types/verify/lighthouse.d.ts +27 -0
  23. package/dist/types/verify/seo-surface.d.ts +23 -0
  24. package/dist/types/verify/serve.d.ts +6 -0
  25. package/dist/verify/budgets.js +180 -0
  26. package/dist/verify/class-coverage.js +114 -0
  27. package/dist/verify/ignore.js +11 -0
  28. package/dist/verify/lighthouse.js +98 -0
  29. package/dist/verify/seo-surface.js +97 -0
  30. package/dist/verify/serve.js +123 -0
  31. package/package.json +16 -10
  32. package/templates/app/.github/workflows/deploy-rsync.yaml.hbs +58 -0
  33. package/templates/app/.oxfmtrc.json +32 -0
  34. package/templates/app/.oxlintrc.json +10 -0
  35. package/templates/app/.vscode/extensions.json +3 -0
  36. package/templates/app/build/Caddyfile.hbs +24 -0
  37. package/templates/app/build/Caddyfile.preview +34 -0
  38. package/templates/app/index.html +35 -0
  39. package/templates/app/logo.svg +143 -0
  40. package/templates/app/pnpm-workspace.yaml +10 -0
  41. package/templates/app/public/favicon-128.png +0 -0
  42. package/templates/app/public/favicon-48.png +0 -0
  43. package/templates/app/public/favicon-96.png +0 -0
  44. package/templates/app/public/favicon.ico +0 -0
  45. package/templates/app/public/images/landing/landing.png +0 -0
  46. package/templates/app/public/robots.txt +5 -0
  47. package/templates/app/src/App.vue +22 -0
  48. package/templates/app/src/pages/Contact.md +25 -0
  49. package/templates/app/src/pages/Error404Page.vue +31 -0
  50. package/templates/app/src/pages/GettingStarted/GettingStarted.md +52 -0
  51. package/templates/app/src/pages/Index.md.hbs +46 -0
  52. package/templates/app/src/router/index.ts +21 -0
  53. package/templates/app/src/router/routes.ts +218 -0
  54. package/templates/app/src/setup.ts +8 -0
  55. package/templates/app/src/shims-virtual.d.ts +67 -0
  56. package/templates/app/src/shims-vue.d.ts +6 -0
  57. package/templates/app/vitrify.config.ts.hbs +249 -0
@@ -0,0 +1,123 @@
1
+ import { createServer } from 'node:http';
2
+ import { createReadStream } from 'node:fs';
3
+ import { readFile, stat } from 'node:fs/promises';
4
+ import { extname, join, normalize, resolve } from 'node:path';
5
+ import { gzipSync } from 'node:zlib';
6
+ /**
7
+ * Minimal static file server for `ssg verify`.
8
+ *
9
+ * `verify` must measure the *built* site the way a real static host serves it,
10
+ * so it serves `dist/static` on an ephemeral port rather than reusing a dev
11
+ * server (which would report dev transforms, not the deployed bytes).
12
+ *
13
+ * "The way a real static host serves it" includes the two things that move
14
+ * Lighthouse most and that a production host always does:
15
+ *
16
+ * - **compression** — Caddy (and every CDN) serves `encode gzip`; a verifier
17
+ * that ships raw bytes makes every byte budget and every LCP look ~3× worse
18
+ * than production, so the score is not a measurement of the site.
19
+ * - **caching** — fingerprinted assets are immutable, so a browser (and
20
+ * Lighthouse's cache audit) must be told that. Without the header the
21
+ * `cache-insight` audit fails for a site whose host would pass it.
22
+ */
23
+ const CONTENT_TYPES = {
24
+ '.html': 'text/html; charset=utf-8',
25
+ '.js': 'text/javascript; charset=utf-8',
26
+ '.mjs': 'text/javascript; charset=utf-8',
27
+ '.css': 'text/css; charset=utf-8',
28
+ '.json': 'application/json; charset=utf-8',
29
+ '.svg': 'image/svg+xml',
30
+ '.png': 'image/png',
31
+ '.jpg': 'image/jpeg',
32
+ '.jpeg': 'image/jpeg',
33
+ '.webp': 'image/webp',
34
+ '.avif': 'image/avif',
35
+ '.ico': 'image/x-icon',
36
+ '.txt': 'text/plain; charset=utf-8',
37
+ '.xml': 'application/xml; charset=utf-8',
38
+ '.woff2': 'font/woff2'
39
+ };
40
+ const COMPRESSIBLE = new Set([
41
+ 'text/html; charset=utf-8',
42
+ 'text/plain; charset=utf-8',
43
+ 'text/css; charset=utf-8',
44
+ 'text/javascript; charset=utf-8',
45
+ 'application/json; charset=utf-8',
46
+ 'application/xml; charset=utf-8',
47
+ 'image/svg+xml'
48
+ ]);
49
+ /** Vite fingerprints assets as `name-<hash>.ext`; those are immutable. */
50
+ const IMMUTABLE = /-[A-Za-z0-9_-]{8,}\.[a-z0-9]+$/;
51
+ function contentType(file) {
52
+ return CONTENT_TYPES[extname(file)] ?? 'application/octet-stream';
53
+ }
54
+ function cacheControl(file, type) {
55
+ if (IMMUTABLE.test(file))
56
+ return 'public, max-age=31536000, immutable';
57
+ if (type.startsWith('text/html'))
58
+ return 'no-cache';
59
+ return 'public, max-age=3600';
60
+ }
61
+ /** Candidate files for a request path, in the order a static host would try. */
62
+ function candidatesFor(root, pathname) {
63
+ const clean = normalize(decodeURIComponent(pathname)).replace(/^(\.\.[/\\])+/, '');
64
+ const base = join(root, clean);
65
+ if (extname(base))
66
+ return [base];
67
+ return [join(base, 'index.html'), `${base}.html`, join(base, 'index.html')];
68
+ }
69
+ export async function startStaticServer(root) {
70
+ const absoluteRoot = resolve(root);
71
+ const server = createServer(async (request, response) => {
72
+ const pathname = new URL(request.url ?? '/', 'http://localhost').pathname;
73
+ const acceptsGzip = /\bgzip\b/.test(request.headers['accept-encoding'] ?? '');
74
+ for (const candidate of candidatesFor(absoluteRoot, pathname)) {
75
+ try {
76
+ const info = await stat(candidate);
77
+ if (!info.isFile())
78
+ continue;
79
+ const type = contentType(candidate);
80
+ const headers = {
81
+ 'content-type': type,
82
+ 'cache-control': cacheControl(candidate, type),
83
+ vary: 'Accept-Encoding'
84
+ };
85
+ if (acceptsGzip && COMPRESSIBLE.has(type)) {
86
+ const body = gzipSync(await readFile(candidate));
87
+ response.writeHead(200, {
88
+ ...headers,
89
+ 'content-encoding': 'gzip',
90
+ 'content-length': body.length
91
+ });
92
+ response.end(body);
93
+ return;
94
+ }
95
+ response.writeHead(200, { ...headers, 'content-length': info.size });
96
+ createReadStream(candidate).pipe(response);
97
+ return;
98
+ }
99
+ catch {
100
+ // Try the next candidate.
101
+ }
102
+ }
103
+ response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
104
+ response.end('Not found');
105
+ });
106
+ await new Promise((done, fail) => {
107
+ server.once('error', fail);
108
+ // Port 0 → the OS picks a free port, so parallel runs cannot collide.
109
+ server.listen(0, '127.0.0.1', () => done());
110
+ });
111
+ const address = server.address();
112
+ if (!address || typeof address === 'string') {
113
+ server.close();
114
+ throw new Error('static server did not bind to a port');
115
+ }
116
+ return {
117
+ url: `http://127.0.0.1:${address.port}`,
118
+ port: address.port,
119
+ close: () => new Promise((done) => {
120
+ server.close(() => done());
121
+ })
122
+ };
123
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "simple-site-generator",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "license": "UNLICENSED",
5
5
  "author": "simsustech",
6
6
  "type": "module",
7
7
  "bin": {
8
+ "ssg": "dist/cli.js",
8
9
  "simple-site-generator": "dist/cli.js"
9
10
  },
10
11
  "main": "dist/cli.js",
@@ -21,19 +22,24 @@
21
22
  },
22
23
  "homepage": "https://github.com/simsustech/simple-site-generator/tree/main/packages/simple-site-generator#readme",
23
24
  "dependencies": {
24
- "@vitrify/tools": "^0.4.0",
25
- "handlebars": "^4.7.8",
26
- "inquirer": "^12.5.0",
25
+ "@inquirer/prompts": "^8.7.4",
26
+ "@vitrify/tools": "^0.5.5",
27
+ "chrome-launcher": "^1.2.2",
28
+ "gray-matter": "^4.0.3",
29
+ "handlebars": "^4.7.10",
30
+ "inquirer": "^14.2.4",
27
31
  "latest-version": "^9.0.0",
28
- "minimist": "^1.2.8"
32
+ "lighthouse": "^13.5.0",
33
+ "minimist": "^1.2.8",
34
+ "zod": "^4.6.7"
29
35
  },
30
36
  "devDependencies": {
31
- "@types/inquirer": "^9.0.7",
37
+ "@types/inquirer": "^9.0.10",
32
38
  "@types/minimist": "^1.2.5",
33
- "@types/node": "^22.13.10",
34
- "typescript": "^5.8.2",
35
- "vite": "^6.2.2",
36
- "vue": "^3.5.13"
39
+ "@types/node": "^26.6.5",
40
+ "typescript": "^7.0.2",
41
+ "vite": "^8.3.5",
42
+ "vue": "^3.5.44"
37
43
  },
38
44
  "files": [
39
45
  "dist",
@@ -0,0 +1,58 @@
1
+ name: Build and deploy through rsync
2
+
3
+ on:
4
+ push:
5
+ branches: ["production"]
6
+
7
+ env:
8
+ working-directory: ./
9
+ url: {{ url }}
10
+
11
+ jobs:
12
+ build:
13
+ name: Build and deploy production
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+
18
+ - name: Creating .npmrc
19
+ run: |
20
+ cat > $\{{env.working-directory}}.npmrc << EOF
21
+ @simsustech:registry=https://npm.simsus.tech
22
+ //npm.simsus.tech/:_authToken=$SIMSUSTECH_NPM_TOKEN
23
+ EOF
24
+ env:
25
+ SIMSUSTECH_NPM_TOKEN: $\{{ secrets.SIMSUSTECH_NPM_TOKEN }}
26
+
27
+ - name: Install pnpm
28
+ # generated projects have no packageManager field, so set the version here
29
+ uses: pnpm/action-setup@v6
30
+ with:
31
+ version: 12
32
+
33
+ - name: Use Node.js
34
+ uses: actions/setup-node@v7
35
+ with:
36
+ node-version: 24
37
+
38
+ - name: Install and build
39
+ working-directory: $\{{ env.working-directory }}
40
+ run: |
41
+ pnpm i
42
+ pnpm run build
43
+
44
+ - name: rsync deployments
45
+ uses: burnett01/rsync-deployments@v9
46
+ with:
47
+ switches: -avzr --delete
48
+ path: $\{{env.working-directory}}dist/static/
49
+ remote_path: /srv/$\{{env.url}}
50
+ remote_host: $\{{ secrets.DEPLOY_HOST }}
51
+ remote_user: $\{{ secrets.DEPLOY_USER }}
52
+ remote_key: $\{{ secrets.DEPLOY_KEY }}
53
+
54
+ - name: Remove .npmrc
55
+ # run even if an earlier step failed: this file holds registry tokens
56
+ if: always()
57
+ # -f: the file may legitimately be gone already
58
+ run: rm -f $\{{env.working-directory}}.npmrc
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "semi": false,
4
+ "tabWidth": 2,
5
+ "singleQuote": true,
6
+ "printWidth": 80,
7
+ "trailingComma": "none",
8
+ "sortPackageJson": false,
9
+ "ignorePatterns": [
10
+ "**/dist/**",
11
+ "**/node_modules/**",
12
+ "**/*.hbs",
13
+ ".github/**",
14
+ "pnpm-lock.yaml",
15
+ "pnpm-workspace.yaml"
16
+ ],
17
+ "overrides": [
18
+ {
19
+ "files": ["*.json5"],
20
+ "options": {
21
+ "singleQuote": false,
22
+ "quoteProps": "preserve"
23
+ }
24
+ },
25
+ {
26
+ "files": ["*.yml", "*.yaml"],
27
+ "options": {
28
+ "singleQuote": false
29
+ }
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
4
+ "categories": {
5
+ "correctness": "error"
6
+ },
7
+ "rules": {
8
+ "@typescript-eslint/ban-ts-comment": "off"
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["vue.volar", "oxc.oxc-vscode"]
3
+ }
@@ -0,0 +1,24 @@
1
+ {{ url }} {
2
+ # The bundler fingerprints every asset as `name-<hash>.ext`; such a file is
3
+ # immutable, so the browser may keep it for a year without revalidating.
4
+ @hashed path_regexp hashed -[A-Za-z0-9_]{8,}\.(js|mjs|css|woff2|png|jpe?g|webp|svg)$
5
+ header @hashed Cache-Control "public, max-age=31536000, immutable"
6
+
7
+ encode zstd gzip
8
+ handle {
9
+ root * /srv/{{ url }}
10
+ try_files {path}.html {path}
11
+ file_server
12
+ }
13
+
14
+ # A missing page has to answer 404. Ending `try_files` in a file — the old
15
+ # `/index.html` fallback, or a `/404.html` one — answers 200, so every
16
+ # mistyped URL is served as a real page and a crawler indexes it. The status
17
+ # a monitor sees would be a lie too.
18
+ handle_errors {
19
+ root * /srv/{{ url }}
20
+ @notFound expression {err.status_code} == 404
21
+ rewrite @notFound /404.html
22
+ file_server
23
+ }
24
+ }
@@ -0,0 +1,34 @@
1
+ # Local preview of the built site — `pnpm preview`, or directly:
2
+ #
3
+ # caddy run --config build/Caddyfile.preview --adapter caddyfile
4
+ #
5
+ # Why a Caddyfile rather than `caddy file-server`: the routes are extensionless
6
+ # (`/about` serves `about.html`), and `file-server` has no `try_files` — it
7
+ # answers 404 for every page URL. This mirrors the shape of `build/Caddyfile`,
8
+ # which is what production runs, so what you check here is what ships.
9
+ #
10
+ # The port is overridable: PREVIEW_PORT=9000 pnpm preview
11
+ :{$PREVIEW_PORT:8095} {
12
+ # `root` is relative to the directory the command runs in, which is this
13
+ # package — so it needs no absolute path and survives a different checkout.
14
+ root * ./dist/static
15
+
16
+ encode gzip
17
+
18
+ # The bundler fingerprints every asset as `name-<hash>.ext`; such a file is
19
+ # immutable, so the browser may keep it for a year without revalidating.
20
+ @hashed path_regexp hashed -[A-Za-z0-9_]{8,}\.(js|mjs|css|woff2|png|jpe?g|webp|svg)$
21
+ header @hashed Cache-Control "public, max-age=31536000, immutable"
22
+
23
+ try_files {path}.html {path}
24
+ file_server
25
+
26
+ # A missing page answers 404 with the site's own error page, rather than
27
+ # ending `try_files` in a file (which would answer 200 — a soft 404).
28
+ handle_errors {
29
+ root * ./dist/static
30
+ @notFound expression {err.status_code} == 404
31
+ rewrite @notFound /404.html
32
+ file_server
33
+ }
34
+ }
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <!--
5
+ `charset` must stay first: the encoding has to be declared inside the
6
+ first 1 KiB, and the SSR-collected meta tags are injected at the end of the
7
+ head element (see the vitrify injectSsrContext fix) so they cannot push
8
+ this line out of that window.
9
+ -->
10
+ <meta charset="UTF-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12
+
13
+ <!--
14
+ Fonts are served by the preset's web-fonts provider (bunny); connecting
15
+ early avoids an extra round-trip on the critical path.
16
+ -->
17
+ <link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
18
+
19
+ <!--
20
+ The Meta plugin owns the document title. A static title element written
21
+ here would be a second one in the built HTML, which the verifier rejects —
22
+ and a crawler would then pick arbitrarily between the two.
23
+
24
+ Keep element literals (an angle-bracketed `title`, `head` or `link`) out of
25
+ these comments: vitrify rewrites the head with tag-matching regular
26
+ expressions, and a tag-shaped word inside a comment is a false match that
27
+ mangles the document.
28
+ -->
29
+ </head>
30
+
31
+ <body>
32
+ <!-- A body opening and closing tag is sufficient for Vitrify -->
33
+ <div id="app"></div>
34
+ </body>
35
+ </html>
@@ -0,0 +1,143 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+ <svg
5
+ width="76.023819mm"
6
+ height="66.642418mm"
7
+ viewBox="0 0 76.02382 66.642419"
8
+ version="1.1"
9
+ id="svg130"
10
+ sodipodi:docname="logo text.svg"
11
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
12
+ inkscape:export-filename="/home/stefan/Nextcloud/logo simsustech4.png"
13
+ inkscape:export-xdpi="44"
14
+ inkscape:export-ydpi="44"
15
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
16
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ xmlns:svg="http://www.w3.org/2000/svg">
19
+ <sodipodi:namedview
20
+ id="namedview132"
21
+ pagecolor="#ffffff"
22
+ bordercolor="#666666"
23
+ borderopacity="1.0"
24
+ inkscape:pageshadow="2"
25
+ inkscape:pageopacity="0.0"
26
+ inkscape:pagecheckerboard="0"
27
+ inkscape:document-units="mm"
28
+ showgrid="false"
29
+ inkscape:zoom="0.5"
30
+ inkscape:cx="281"
31
+ inkscape:cy="-298"
32
+ inkscape:window-width="2560"
33
+ inkscape:window-height="1379"
34
+ inkscape:window-x="0"
35
+ inkscape:window-y="33"
36
+ inkscape:window-maximized="1"
37
+ inkscape:current-layer="layer1"
38
+ fit-margin-top="3"
39
+ fit-margin-left="3"
40
+ fit-margin-right="3"
41
+ fit-margin-bottom="3" />
42
+ <defs
43
+ id="defs127" />
44
+ <g
45
+ inkscape:label="Layer 1"
46
+ inkscape:groupmode="layer"
47
+ id="layer1"
48
+ transform="translate(308.77352,-114.8964)">
49
+ <g
50
+ id="g8406-0"
51
+ transform="matrix(0.03124864,-0.05412422,0.05412422,0.03124864,-375.29155,156.30058)">
52
+ <path
53
+ sodipodi:type="star"
54
+ style="fill:#22c92a;fill-opacity:1;stroke:#111115;stroke-width:64.7013;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
55
+ id="path4686-5-0-6"
56
+ inkscape:flatsided="true"
57
+ sodipodi:sides="6"
58
+ sodipodi:cx="-1711.292"
59
+ sodipodi:cy="-2891.6067"
60
+ sodipodi:r1="1941.0391"
61
+ sodipodi:r2="1680.9893"
62
+ sodipodi:arg1="1.0471975"
63
+ sodipodi:arg2="1.5707963"
64
+ inkscape:rounded="0"
65
+ inkscape:randomized="0"
66
+ transform="matrix(0.28316651,0,0,0.28316651,1432.8627,2202.612)"
67
+ inkscape:transform-center-x="1.6003776e-05"
68
+ inkscape:transform-center-y="-1.5044873e-05"
69
+ d="m -740.77237,-1210.6176 -1941.03903,10e-5 -970.5197,-1680.9891 970.5195,-1680.9892 1941.03905,-1e-4 970.51962,1680.9891 z" />
70
+ <path
71
+ style="fill:none;stroke:#000000;stroke-width:0.161583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
72
+ d="M 673.46343,907.80596 H 856.6758"
73
+ id="path4451-3-6-2-2" />
74
+ <path
75
+ style="fill:none;stroke:#000000;stroke-width:0.242375px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
76
+ d="m 398.74983,1383.7452 137.4093,237.9999"
77
+ id="path5002-2-3-6" />
78
+ <path
79
+ style="fill:none;stroke:#000000;stroke-width:0.242375px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
80
+ d="m 673.46343,907.80596 274.81862,10e-5"
81
+ id="path5002-5-9-7-1" />
82
+ <path
83
+ style="fill:none;stroke:#000000;stroke-width:0.242375px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
84
+ d="M 1223.1007,1859.8056 1360.51,1621.8057"
85
+ id="path5002-3-1-5-8" />
86
+ <path
87
+ style="fill:none;stroke:#000000;stroke-width:0.48475px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
88
+ d="M 536.15913,1621.7451 948.28205,907.80606 1360.51,1621.8057 Z"
89
+ id="path5157-2-9-7" />
90
+ <path
91
+ style="fill:#d67915;fill-opacity:1;stroke:#000000;stroke-width:0.48475px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
92
+ d="m 948.28205,907.80606 274.81865,-2e-4 274.8186,475.99984 -137.4093,238 -412.22795,-713.99964"
93
+ id="path5272-7-2-9" />
94
+ <path
95
+ style="fill:#00a4e6;fill-opacity:1;stroke:#000000;stroke-width:0.48475px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
96
+ d="m 948.28205,907.80606 -274.81861,-10e-5 -274.81861,475.99984 137.5143,237.9393 412.12292,-713.93904"
97
+ id="path5274-0-2-2" />
98
+ <path
99
+ style="fill:#eece0c;fill-opacity:1;stroke:#000000;stroke-width:0.48475px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
100
+ d="m 536.15913,1621.7451 137.30434,238.0605 h 549.63723 l 137.4093,-237.9999 -824.35087,-0.061"
101
+ id="path5276-9-8-0" />
102
+ <path
103
+ style="fill:none;stroke:#000000;stroke-width:18.3212;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
104
+ d="m 673.46342,907.80601 549.63728,-8e-5 274.8186,475.99977 -274.8186,475.9999 -549.63723,10e-5"
105
+ id="path7069-9-2" />
106
+ <path
107
+ style="fill:none;stroke:#000000;stroke-width:0.48475px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
108
+ d="M 673.46346,1859.8057 398.6448,1383.8058 673.46342,907.80601"
109
+ id="path7071-7-3" />
110
+ <path
111
+ style="fill:none;stroke:#000000;stroke-width:18.3212;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
112
+ d="M 673.46342,907.80601 398.64479,1383.8059 673.46344,1859.8058"
113
+ id="path7281-3-7" />
114
+ <path
115
+ style="fill:none;stroke:#000000;stroke-width:18.3208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
116
+ d="m 965.83267,938.18694 c -10.97866,-19.01559 -24.1009,-19.03555 -33.40914,-2.91319"
117
+ id="path33462"
118
+ sodipodi:nodetypes="cc" />
119
+ <path
120
+ style="fill:none;stroke:#000000;stroke-width:18.3208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
121
+ d="m 1342.9801,1591.4734 c 10.9786,19.0155 4.4348,30.3897 -14.1817,30.3897"
122
+ id="path33462-2"
123
+ sodipodi:nodetypes="cc" />
124
+ <path
125
+ style="fill:none;stroke:#000000;stroke-width:18.3208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
126
+ d="m 565.71911,1621.69 c -18.40838,0 -24.03457,-9.498 -15.36275,-24.5181"
127
+ id="path40533"
128
+ sodipodi:nodetypes="cc" />
129
+ <path
130
+ style="fill:none;stroke:#000000;stroke-width:18.3208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
131
+ d="m 565.71916,1621.69 763.07924,0.1731"
132
+ id="path43135" />
133
+ <path
134
+ style="fill:none;stroke:#000000;stroke-width:18.3208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
135
+ d="M 550.35638,1597.1719 932.42356,935.27375"
136
+ id="path43137" />
137
+ <path
138
+ style="fill:none;stroke:#000000;stroke-width:18.3208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
139
+ d="M 965.83269,938.18695 1342.9789,1591.4741"
140
+ id="path43139" />
141
+ </g>
142
+ </g>
143
+ </svg>
@@ -0,0 +1,10 @@
1
+ # pnpm 12 does not run a dependency's build scripts unless they are allowlisted,
2
+ # and it fails the install when one is skipped. These need theirs:
3
+ # - esbuild / sharp select and link a prebuilt native binary at install time,
4
+ # - @parcel/watcher compiles its native watcher.
5
+ # Vitrify's dev server uses esbuild, so without these the project installs but
6
+ # cannot build.
7
+ allowBuilds:
8
+ '@parcel/watcher': true
9
+ esbuild: true
10
+ sharp: true
Binary file
@@ -0,0 +1,5 @@
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /404
4
+
5
+ Sitemap: /sitemap.xml
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <router-view v-slot="{ Component }">
3
+ <Suspense>
4
+ <template #default>
5
+ <component :is="Component" />
6
+ </template>
7
+ <template #fallback>
8
+ <div>Loading...</div>
9
+ </template>
10
+ </Suspense>
11
+ </router-view>
12
+ </template>
13
+
14
+ <script lang="ts">
15
+ export default {
16
+ name: 'App'
17
+ }
18
+ </script>
19
+
20
+ <script setup lang="ts"></script>
21
+
22
+ <style lang="sass"></style>
@@ -0,0 +1,25 @@
1
+ ---
2
+ id: contact
3
+ title: Contact
4
+ description: How to reach us, including the project repository and support channels.
5
+ label: Contact
6
+ # Prerendered like every other page, so it gets a real <h1> and its own
7
+ # <head> tags. A content page should not be client-only: `noSsr` would make it
8
+ # invisible to a crawler and would fail `ssg verify`.
9
+ tags:
10
+ - contact
11
+ - footer
12
+ lists:
13
+ - header: Links
14
+ items:
15
+ - overline: Source
16
+ label: https://github.com/simsustech/simple-site-generator
17
+ icon:
18
+ name: i-mdi-github
19
+ ---
20
+
21
+ ## Get in touch
22
+
23
+ Add your own contact details here — a phone number, an email address and your
24
+ opening hours. Anything you put in the `ssg.extra` block of `vitrify.config.ts`
25
+ is also mirrored into `LocalBusiness` structured data automatically.
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <main
3
+ class="ssg-section flex flex-center text-center min-h-[60vh] bg-[var(--q-surface-container-low)] text-[var(--q-on-surface)]"
4
+ >
5
+ <div class="ssg-section__inner">
6
+ <!--
7
+ A real `h1`: the 404 page is a page like any other, and `ssg verify`
8
+ requires exactly one `h1` on every page. Using a styled `div` here would
9
+ make the site fail its own check.
10
+ -->
11
+ <h1 class="text-[clamp(5rem,18vw,10rem)] leading-none m-0">404</h1>
12
+
13
+ <p class="text-step-1 opacity-80 mt-space-2 mb-0">Sorry, nothing here.</p>
14
+
15
+ <!--
16
+ The kit's own action: it carries the primary/on-primary pair, so this
17
+ button cannot drift into a hand-picked colour. An earlier version of this
18
+ page used `color="white" text-color="blue"`, which measured 2.54:1 and put
19
+ a Quasar-blue label on a green page.
20
+ -->
21
+ <CtaButton class="mt-space-4" label="Go home" route="/" />
22
+ </div>
23
+ </main>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { CtaButton } from '@simple-site-generator/templates'
28
+
29
+ // Keep the name — `ssg check` and the router both key off it.
30
+ defineOptions({ name: 'Error404Page' })
31
+ </script>