simple-site-generator 0.1.3 → 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 +14 -8
  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,52 @@
1
+ ---
2
+ id: gettingstarted
3
+ title: Getting started
4
+ description: Scaffold a project, add pages and verify the build before you deploy.
5
+ label: Getting started
6
+ icon:
7
+ name: i-mdi-rocket-launch
8
+ tags:
9
+ - menu
10
+ ---
11
+
12
+ ## Scaffold a project
13
+
14
+ ```bash
15
+ pnpm dlx simple-site-generator init my-site --template landing
16
+ cd my-site
17
+ pnpm install
18
+ pnpm dev
19
+ ```
20
+
21
+ Replace `landing` with `docs` or `business` to start from a different template.
22
+
23
+ ## Add a page
24
+
25
+ ```bash
26
+ ssg add-page services --title "Services" --tags menu
27
+ ```
28
+
29
+ This writes `src/pages/services.md` with frontmatter that already satisfies the
30
+ page contract. Add `--page Landing` to render it with the conversion layout.
31
+
32
+ ## Check, build, verify
33
+
34
+ ```bash
35
+ ssg check # validate every page's frontmatter
36
+ ssg build # prerender the site into dist/static
37
+ ssg verify # SEO surface, byte budgets and Lighthouse
38
+ ```
39
+
40
+ `ssg verify` fails the run on a broken SEO surface (a missing description, two
41
+ titles, unparseable JSON-LD) or on an exceeded byte budget, and warns when
42
+ Lighthouse performance is below target.
43
+
44
+ ## Deploy
45
+
46
+ The build output is `dist/static` — plain files. Copy them to any static host.
47
+ The included GitHub workflow uses `rsync` to push to your own server; define
48
+ `DEPLOY_HOST`, `DEPLOY_USER` and `DEPLOY_KEY` as repository secrets.
49
+
50
+ > **Note**
51
+ > Change the `template` value in `vitrify.config.ts` to switch between `docs`,
52
+ > `landing` and `business`.
@@ -0,0 +1,46 @@
1
+ ---
2
+ id: home
3
+ title: Home
4
+ description: A fast, static website built with Quasar and Vitrify — edit the
5
+ content as Markdown and deploy the generated files anywhere.
6
+ label: Home
7
+ {{#if landingPage}}
8
+ page: Landing
9
+ {{/if}}
10
+ tags:
11
+ - menu
12
+ icon:
13
+ name: i-mdi-home
14
+ cta:
15
+ message: Get started
16
+ route: /gettingstarted
17
+ cards:
18
+ - header: Content in Markdown
19
+ content: Write pages as Markdown files with frontmatter; the templates turn
20
+ them into a real site.
21
+ - header: Static and fast
22
+ content: Every page is prerendered to HTML, so it loads without waiting on
23
+ JavaScript.
24
+ - header: One command to verify
25
+ content: ssg verify checks the SEO surface, the byte budgets and Lighthouse
26
+ before you deploy.
27
+ images:
28
+ - name: Landing
29
+ src: images/landing/landing.png
30
+ caption: A simple site generator project
31
+ # The hero's animated process graphic — a route with named stops, drawn from
32
+ # this list. Replace the stops with your own process; two to five read best.
33
+ # `landing` shows it only when a page declares no hero image, and `business`
34
+ # shows it in the page's title band instead of a background illustration.
35
+ demo:
36
+ label: From Markdown to a deployed site
37
+ steps:
38
+ - Write Markdown
39
+ - Build
40
+ - Deploy
41
+ ---
42
+
43
+ ## Why use this
44
+
45
+ Write Markdown, get a fast static site. The pages below are the starting point —
46
+ replace them with your own content.
@@ -0,0 +1,21 @@
1
+ /// <reference types="vitrify/client" />
2
+ import {
3
+ createMemoryHistory,
4
+ createRouter as _createRouter,
5
+ createWebHistory
6
+ } from 'vue-router'
7
+ import routes from './routes'
8
+
9
+ export default function createRouter() {
10
+ return _createRouter({
11
+ // use appropriate history implementation for server/client
12
+ // import.meta.env.SSR is injected by Vite.
13
+ history: import.meta.env.SSR
14
+ ? createMemoryHistory(__BASE_URL__)
15
+ : createWebHistory(__BASE_URL__),
16
+ scrollBehavior(_to, _from, _savedPosition) {
17
+ return { top: 0 }
18
+ },
19
+ routes
20
+ })
21
+ }
@@ -0,0 +1,218 @@
1
+ import type { RouteRecordRaw } from 'vue-router'
2
+ // The slim virtual module carries ONLY the site identity the client needs.
3
+ // Importing `virtual:vitrify-config` instead inlined the entire build config
4
+ // (including the whole UnoCSS/Quasar option tree) into the browser bundle —
5
+ // 205 KB raw / 109 KB minified on the previous version of this file.
6
+ import { site } from 'virtual:ssg:site'
7
+ import { QNoSsr } from 'quasar'
8
+ import { defineAsyncComponent, h } from 'vue'
9
+ import { Layout } from 'virtual:ssg:template'
10
+
11
+ const { tags, extra } = site
12
+
13
+ /** Frontmatter a page file may declare (see CONTENT-BLUEPRINT.md). */
14
+ interface PageFrontmatter {
15
+ id?: string
16
+ title?: string
17
+ label?: string
18
+ description?: string
19
+ /** Sort weight; the home page is always first. */
20
+ order?: number
21
+ /** Name of the template component to render, e.g. `Landing`. */
22
+ page?: string
23
+ /** Render this page client-side only. */
24
+ noSsr?: boolean
25
+ /** Keep the page out of the index and the sitemap. */
26
+ noindex?: boolean
27
+ image?: string
28
+ /**
29
+ * BCP-47 tag when this page is not in the site's default language, e.g. `nl`.
30
+ * Drives `<html lang>`, `og:locale` and the JSON-LD `inLanguage`.
31
+ */
32
+ lang?: string
33
+ /**
34
+ * Other-language versions of this page: `{ lang, path, label? }`.
35
+ *
36
+ * Emitted as `rel="alternate"` hreflang links, and rendered as the language
37
+ * switch in the header (which hides the entry pointing at the current page,
38
+ * so a single-language site shows no switch at all).
39
+ *
40
+ * The pairing is app-specific — declare it per page, or derive it here as
41
+ * `simsustech/website` does for its `nl-`-prefixed pages.
42
+ */
43
+ alternates?: { lang: string; path: string; label?: string }[]
44
+ breadcrumbs?: string[]
45
+ tags?: string[]
46
+ [key: string]: unknown
47
+ }
48
+
49
+ interface PageModule {
50
+ attributes?: PageFrontmatter
51
+ html?: string
52
+ default?: unknown
53
+ }
54
+
55
+ /**
56
+ * Markdown and Vue pages, eagerly imported so the route table is complete at
57
+ * module-evaluation time (prerendering needs every route to exist up front).
58
+ *
59
+ * The pattern MUST be relative. The previous version was a bare `src/pages` path,
60
+ * which Vite 8 rejects outright — "Invalid glob … It must start with '/' or
61
+ * './'" — so the glob resolved to nothing and every page silently disappeared
62
+ * from the build. The extglob alternation that also changed is NOT the cause: the
63
+ * extglob and brace forms match identically once the leading './' is present, and
64
+ * `test/page-glob.test.ts` pins that.
65
+ *
66
+ * Note for editors: never write a glob containing a `*` immediately followed by a
67
+ * `/` inside a block comment — the `*` + `/` pair closes the comment early and
68
+ * the rest of the comment is parsed as code.
69
+ */
70
+ const pagesImport = import.meta.glob(
71
+ ['../pages/**/*.{md,vue}', '!**/Error404Page.vue'],
72
+ { eager: true }
73
+ )
74
+
75
+ const noSsrComponent = {
76
+ name: 'NoSSRWrapper',
77
+ render(this: { component: unknown; page: unknown }) {
78
+ const component = defineAsyncComponent(this.component as never)
79
+ return h(QNoSsr, null, {
80
+ default: () => h(component, this.page as Record<string, unknown>)
81
+ })
82
+ },
83
+ props: ['page', 'component']
84
+ }
85
+
86
+ function attributesOf(page: unknown): PageFrontmatter {
87
+ return (page as PageModule | undefined)?.attributes ?? {}
88
+ }
89
+
90
+ /**
91
+ * Home first, then pages with an explicit `order`, then the rest alphabetically.
92
+ */
93
+ const orderedPages = [
94
+ ...Object.entries(pagesImport)
95
+ .filter(([, page]) => {
96
+ const { id, order = 0 } = attributesOf(page)
97
+ return order || id === 'home'
98
+ })
99
+ .sort(([, pageA], [, pageB]) => {
100
+ const { id: idA, order: orderA = 0 } = attributesOf(pageA)
101
+ const { id: idB, order: orderB = 0 } = attributesOf(pageB)
102
+
103
+ if (idA === 'home') return -1
104
+ if (idB === 'home') return 1
105
+
106
+ return orderA - orderB
107
+ }),
108
+ ...Object.entries(pagesImport).filter(([, page]) => {
109
+ const { id, order = 0 } = attributesOf(page)
110
+ return !order && id && id !== 'home'
111
+ })
112
+ ]
113
+
114
+ const pageList = orderedPages.map(([, page]) => attributesOf(page))
115
+
116
+ const routes: RouteRecordRaw[] = [
117
+ {
118
+ path: '/',
119
+ component: Layout,
120
+ props: {
121
+ ...site,
122
+ pages: pageList,
123
+ tags,
124
+ extra
125
+ },
126
+ children: [
127
+ ...orderedPages.map(([, page]) => {
128
+ const { id = '', page: componentName = '', noSsr } = attributesOf(page)
129
+ const { attributes = {}, html = '' } = (page ?? {}) as PageModule
130
+
131
+ const vuePage = async () => {
132
+ if ((page as PageModule).default) {
133
+ return (page as PageModule).default
134
+ }
135
+
136
+ return import('virtual:ssg:template').then((module) => {
137
+ const templates = module.default
138
+
139
+ // No component named: the template's `Page` is the default, which is
140
+ // what most pages want.
141
+ if (!componentName) return module.Page
142
+
143
+ const component = templates[componentName]
144
+
145
+ // A named component that does not exist used to fall back to
146
+ // `module.Page` silently. That is the worst possible outcome for a
147
+ // field that decides a page's entire layout: the page renders, looks
148
+ // plausible, and is the wrong template.
149
+ //
150
+ // Thrown during prerender, so it fails the build rather than
151
+ // shipping.
152
+ if (!component) {
153
+ const available = Object.keys(templates)
154
+ .sort((a, b) => a.localeCompare(b))
155
+ .join(', ')
156
+
157
+ throw new Error(
158
+ `Unknown page template component "${componentName}" on page "${id}".\n` +
159
+ ` \`page:\` names a component from the configured template module,\n` +
160
+ ` which provides: ${available}. Most pages should omit \`page:\`\n` +
161
+ ` entirely and get the template's default \`Page\`.`
162
+ )
163
+ }
164
+
165
+ return component
166
+ })
167
+ }
168
+
169
+ const props = {
170
+ ...attributes,
171
+ content: html,
172
+ pages: pageList,
173
+ // The business details (phone, reviews, service area) live on the site
174
+ // config, not in each page's frontmatter, but the landing page needs them
175
+ // for its proof/CTA sections — so they are passed down here rather than
176
+ // duplicated into every page.
177
+ extra
178
+ }
179
+
180
+ return {
181
+ // The home page lives at the parent path, so its child path is empty.
182
+ path: id === 'home' ? '' : id,
183
+ name: id,
184
+ component: noSsr ? noSsrComponent : vuePage,
185
+ props: noSsr ? { page: props, component: vuePage } : props,
186
+ alias: id === 'home' ? ['home'] : [],
187
+ /**
188
+ * The single source of truth about a page, read by both the shell (for
189
+ * `<head>` tags) and the build (for the sitemap).
190
+ */
191
+ meta: {
192
+ id,
193
+ title: attributes.title,
194
+ description: attributes.description,
195
+ noindex: attributes.noindex ?? false,
196
+ image: attributes.image,
197
+ lang: attributes.lang,
198
+ alternates: attributes.alternates,
199
+ breadcrumbs: attributes.breadcrumbs
200
+ }
201
+ } as RouteRecordRaw
202
+ }),
203
+
204
+ // The catch-all is a CHILD of the layout on purpose. As a sibling it
205
+ // rendered outside the shell, so it had no header, no footer and — because
206
+ // `useSeo` lives in the layout — no `<head>` tags at all. Vue Router ranks
207
+ // a catch-all last, so it cannot shadow a real page.
208
+ {
209
+ path: ':catchAll(.*)*',
210
+ name: 'not-found',
211
+ component: () => import('../pages/Error404Page.vue'),
212
+ meta: { id: '404', title: 'Page not found', noindex: true }
213
+ }
214
+ ]
215
+ }
216
+ ]
217
+
218
+ export default routes
@@ -0,0 +1,8 @@
1
+ import type { FastifyInstance } from 'fastify'
2
+
3
+ /**
4
+ * Only used in SSR/SSG
5
+ */
6
+ export default async function (_fastify: FastifyInstance) {
7
+ console.log('Running setup function....')
8
+ }
@@ -0,0 +1,67 @@
1
+ // Types for the virtual modules the build injects. Without these, every import
2
+ // from `virtual:ssg:*` is an unresolved module in the editor and in any
3
+ // type-check of this app.
4
+
5
+ declare module 'virtual:ssg:site' {
6
+ interface SsgTag {
7
+ label: string
8
+ }
9
+
10
+ interface SsgExtra {
11
+ phone?: string
12
+ email?: string
13
+ address?: string
14
+ instagramUrl?: string
15
+ facebookUrl?: string
16
+ mapUrl?: string
17
+ rating?: number
18
+ reviewCount?: number
19
+ reviewSource?: string
20
+ }
21
+
22
+ interface SsgBusiness {
23
+ type?: string
24
+ name?: string
25
+ description?: string
26
+ telephone?: string
27
+ email?: string
28
+ address?: {
29
+ streetAddress?: string
30
+ postalCode?: string
31
+ addressLocality?: string
32
+ addressCountry?: string
33
+ }
34
+ areaServed?: string[]
35
+ openingHours?: { days: string[]; opens: string; closes: string }[]
36
+ priceRange?: string
37
+ }
38
+
39
+ /**
40
+ * Site identity only. Deliberately NOT the whole build config: this module is
41
+ * evaluated in the browser, so anything added here ships to every visitor.
42
+ */
43
+ export const site: {
44
+ title: string
45
+ url: string
46
+ favicon?: string
47
+ description?: string
48
+ lang?: string
49
+ image?: string
50
+ twitter?: string
51
+ template: string
52
+ templatePackage: string
53
+ tags: Record<string, SsgTag>
54
+ extra?: SsgExtra
55
+ jsonLd?: { business?: SsgBusiness }
56
+ }
57
+ }
58
+
59
+ declare module 'virtual:ssg:template' {
60
+ import type { Component } from 'vue'
61
+
62
+ export const Layout: Component
63
+ export const Page: Component
64
+ /** Optional page components a template may expose, e.g. `Landing`. */
65
+ const template: Record<string, Component>
66
+ export default template
67
+ }
@@ -0,0 +1,6 @@
1
+ // Mocks all files ending in `.vue` showing them as plain Vue instances
2
+ declare module '*.vue' {
3
+ import type { ComponentOptions } from 'vue'
4
+ const component: ComponentOptions
5
+ export default component
6
+ }
@@ -0,0 +1,249 @@
1
+ import type { VitrifyConfig, VitrifyConfigAsync } from 'vitrify'
2
+ import { QuasarPlugin, type QuasarPluginOptions } from 'vitrify/plugins'
3
+ import type { QuasarIconSet } from 'quasar'
4
+ // `QuasarPreset` is exported by the preset, NOT by vitrify. Importing it from
5
+ // 'vitrify' fails at load time ("does not provide an export named
6
+ // 'QuasarPreset'"), and since 0.7 the preset also refuses to run without a
7
+ // style, so both the import and `style` below are required.
8
+ import { QuasarPreset, quasarIconSet } from 'unocss-preset-quasar'
9
+ import { MaterialDesign3 } from 'unocss-preset-quasar/styles'
10
+ import { certificateFor } from 'devcert'
11
+ import { plugin as mdPlugin } from 'vite-plugin-markdown'
12
+ import { Mode } from 'vite-plugin-markdown'
13
+ import { copyFile } from 'fs/promises'
14
+ import { ssgSitemap } from '@simple-site-generator/templates/seo'
15
+ import { ssgUnoConfig } from '@simple-site-generator/templates/design'
16
+
17
+ interface SimpleSiteGenerator {
18
+ ssg: {
19
+ title: string
20
+ url: string
21
+ /** Site description: the sitemap's, and the fallback meta description. */
22
+ description: string
23
+ template: string
24
+ templatePackage: string
25
+ favicon: string
26
+ lang: string
27
+ sourceColor: string
28
+ tags: Record<string, { label: string }>
29
+ extra?: Record<string, unknown>
30
+ jsonLd?: { business?: Record<string, unknown> }
31
+ }
32
+ }
33
+
34
+ // Annotated with the interface rather than left to inference: the three reads
35
+ // of `ssg.description`, `ssg.extra` and `ssg.jsonLd` below happen on values a
36
+ // bare object literal does not have, so an unannotated literal makes every
37
+ // generated project start with five `Property … does not exist` errors.
38
+ const ssg: SimpleSiteGenerator['ssg'] = {
39
+ title: '{{ productName }}',
40
+ url: '{{ url }}',
41
+ description: '{{ description }}',
42
+ favicon: '/favicon-96.png',
43
+ // Rendered from the `--template` value passed to `ssg init`. It selects which
44
+ // template module the app imports, so it must be templated, not hardcoded.
45
+ template: '{{ template }}',
46
+ templatePackage: '@simple-site-generator/templates',
47
+ // BCP-47 tag for og:locale, JSON-LD `inLanguage`, and — through
48
+ // `vitrify.lang` below — the `<html lang>` that Quasar writes. It must name
49
+ // one of Quasar's language packs (node_modules/quasar/lang), e.g. `en-US`,
50
+ // `en-GB`, `nl`; a bare `en` has no pack and Quasar falls back to `en-US`.
51
+ lang: 'en-US',
52
+ // The only colour input. MD3 derives its whole tonal palette — every role
53
+ // token the design system reads, in light and dark mode — from this one value.
54
+ // A `colors` map beside it looks load-bearing and is not: it produced utility
55
+ // classes that drifted away from the templates' own tokens, so there is no
56
+ // second place to set a colour.
57
+ sourceColor: '#22c92a',
58
+ tags: {
59
+ menu: {
60
+ label: 'Menu'
61
+ },
62
+ footer: {
63
+ label: 'Footer'
64
+ },
65
+ contact: {
66
+ label: 'Contact'
67
+ }
68
+ }
69
+ }
70
+
71
+ const { template, templatePackage, title } = ssg
72
+
73
+ /**
74
+ * Quasar's own icons, resolved to iconify classes.
75
+ *
76
+ * Handing Quasar the library *name* lets it pick its own font classes
77
+ * (`mdi mdi-menu-down`), and this app loads no icon font — so every icon the
78
+ * framework draws itself (the dropdown arrow, a select's chevron, a table's
79
+ * expander) renders as nothing. The rewritten set fixes that: each value is a
80
+ * class the preset emits CSS for.
81
+ *
82
+ * The resolution's `iconMapFn` is deliberately not passed — Vitrify serialises
83
+ * the Quasar config into the client bundle and a function does not survive, so
84
+ * passing it makes the arrow throw instead of render. Bare icon names need that
85
+ * mapper; write `i-mdi-*` names instead.
86
+ */
87
+ const icons = quasarIconSet('mdi-v7')
88
+
89
+ const quasarConf: QuasarPluginOptions = {
90
+ framework: {
91
+ components: [],
92
+ // 'Meta' is what makes Quasar emit the per-page <title>, description,
93
+ // canonical, OpenGraph and JSON-LD tags. Without it `ssrContext._meta.headTags`
94
+ // stays empty and a prerendered page ships no metadata at all.
95
+ plugins: ['Dialog', 'Notify', 'Loading', 'Meta'],
96
+ // A Quasar icon-library name. The preset derives the nested icon set from
97
+ // `quasar/dist/icon-set`, rewrites each value to its iconify class and
98
+ // safelists them — so the ~200-line hand-written icon map is not needed.
99
+ // The rewritten set is what Quasar renders from, above; the preset types it
100
+ // as `unknown`, so the assertion to Quasar's own type belongs here.
101
+ iconSet: icons.iconSet as QuasarIconSet
102
+ },
103
+ /*
104
+ * Disable SASS if you use UnoCSS
105
+ */
106
+ disableSass: true
107
+ }
108
+
109
+ export default async function ({
110
+ mode
111
+ }: Parameters<VitrifyConfigAsync>[0]): Promise<VitrifyConfig> {
112
+ const config: VitrifyConfig & SimpleSiteGenerator = {
113
+ optimizeDeps: {
114
+ exclude: ['@simple-site-generator/templates']
115
+ },
116
+ plugins: [
117
+ mdPlugin({
118
+ mode: [Mode.HTML]
119
+ }),
120
+ // Emits sitemap.xml and robots.txt into dist/static, generated from the
121
+ // same page frontmatter the router reads.
122
+ ssgSitemap({
123
+ site: {
124
+ title: ssg.title,
125
+ url: ssg.url,
126
+ description: ssg.description,
127
+ favicon: ssg.favicon,
128
+ lang: ssg.lang,
129
+ extra: ssg.extra,
130
+ jsonLd: ssg.jsonLd
131
+ },
132
+ pagesDir: new URL('./src/pages', import.meta.url).pathname,
133
+ distDir: new URL('./dist', import.meta.url).pathname
134
+ }),
135
+ {
136
+ name: 'simple-site-generator-plugin',
137
+ resolveId(id: string) {
138
+ if (id === 'virtual:ssg:template' || id === 'virtual:ssg:site') {
139
+ return `\0${id}`
140
+ }
141
+ },
142
+ load(id: string) {
143
+ if (id === '\0virtual:ssg:template') {
144
+ return `import * as template from '${templatePackage}/${template}';
145
+ export * from '${templatePackage}/${template}';
146
+ export default template;`
147
+ }
148
+ if (id === '\0virtual:ssg:site') {
149
+ // ONLY the identity the browser needs. The previous version imported
150
+ // `virtual:vitrify-config`, which inlined the whole build config
151
+ // (UnoCSS presets, Quasar options, icon map) into the client bundle.
152
+ // The client reads exactly these fields, so anything else added here
153
+ // would ship to every visitor.
154
+ return `export const site = ${JSON.stringify({
155
+ title: ssg.title,
156
+ url: ssg.url,
157
+ favicon: ssg.favicon,
158
+ lang: ssg.lang,
159
+ template: ssg.template,
160
+ templatePackage: ssg.templatePackage,
161
+ tags: ssg.tags,
162
+ extra: ssg.extra,
163
+ jsonLd: ssg.jsonLd
164
+ })};`
165
+ }
166
+ },
167
+ closeBundle: () => {
168
+ copyFile(
169
+ new URL('./build/Caddyfile', import.meta.url).pathname,
170
+ new URL(`./dist/static/Caddyfile`, import.meta.url).pathname
171
+ )
172
+ }
173
+ }
174
+ ],
175
+ ssg,
176
+ vitrify: {
177
+ // Registering the Quasar plugin is what makes `useQuasar()` and every
178
+ // `q-*` component work during SSR. Without it a prerendered page throws
179
+ // "Cannot read properties of undefined (reading 'screen')" and ships an
180
+ // empty <div id="app">.
181
+ plugins: [
182
+ {
183
+ plugin: QuasarPlugin,
184
+ options: quasarConf
185
+ }
186
+ ],
187
+ productName: title,
188
+ // Pass the BCP-47 tag to Quasar as well: Quasar writes `<html lang>`
189
+ // from its selected language pack, and vitrify forwards this value to it.
190
+ lang: ssg.lang,
191
+ hooks: {
192
+ onSetup: [new URL('src/setup.ts', import.meta.url)]
193
+ },
194
+ unocss: {
195
+ presets: [
196
+ QuasarPreset({
197
+ style: MaterialDesign3,
198
+ sourceColor: ssg.sourceColor,
199
+ plugins: quasarConf.framework.plugins,
200
+ iconSet: 'mdi-v7',
201
+ // Only the third-party Quasar UI libraries whose styles this app
202
+ // needs contribute CSS; declaring them is what ports their
203
+ // selectors. The prose styles are not among them: the design system
204
+ // styles the markdown output itself (`ssg-prose`), so no Markdown
205
+ // component or stylesheet is needed on either side.
206
+ appExtensions: ['qmediaplayer']
207
+ })
208
+ ],
209
+ // The templates' design system: theme scale, shortcuts, preflights and
210
+ // the safelist of its class names.
211
+ ...ssgUnoConfig,
212
+ theme: ssgUnoConfig.theme,
213
+ content: {
214
+ pipeline: {
215
+ include: [
216
+ /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
217
+ // The templates package IS scanned, and has to be: a template
218
+ // component renders `q-separator` and `i-mdi-phone` as readily as
219
+ // it renders an `ssg-*` class, and `unocss-preset-quasar` is what
220
+ // turns both into CSS. Measuring this include against the site's
221
+ // own sources showed 137 selectors that exist *only* because of
222
+ // it — `q-separator`, `q-card__actions--*`, `q-btn-group*` and the
223
+ // icons among them. Dropping it looks like a free 17 KB and is
224
+ // actually an unstyled drawer with missing icons.
225
+ //
226
+ // The shortcut names are safelisted separately (`ssgSafelist`), so
227
+ // it is not the shortcut CSS this supplies — it is the Quasar and
228
+ // icon classes. The noise that rides along with it is bounded by
229
+ // the design system's own `blocklist`.
230
+ new RegExp(`${ssg.templatePackage}.*\\.js`)
231
+ ]
232
+ }
233
+ }
234
+ },
235
+ // No `sass.global` block: the media player's styles come from the preset's
236
+ // `appExtensions`, and the old `QMarkdown.sass` path does not exist (the
237
+ // file is `.scss`), so it only produced a resolution error.
238
+ ssr: {
239
+ serverModules: ['vite-plugin-markdown']
240
+ }
241
+ }
242
+ }
243
+ if (mode === 'development') {
244
+ config.server = {
245
+ https: await certificateFor('vitrify.test')
246
+ }
247
+ }
248
+ return config
249
+ }