@vyriy/ssg 0.9.0 → 0.9.2

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/AGENTS.md CHANGED
@@ -14,6 +14,7 @@ Always read the root `AGENTS.md` first for current project direction, then use t
14
14
  - Avoid hidden framework, CMS, browser, filesystem, network, or cloud assumptions unless they are the package contract.
15
15
  - Add packages only when they reduce real complexity, clarify boundaries, or improve reuse.
16
16
  - Prefer the option that is simpler to explain, easier to evolve, and calmer to maintain.
17
+ - Grow package functionality in small, calm, reviewable steps that are easy to test and continue.
17
18
 
18
19
  ## Standard Package Shape
19
20
 
@@ -84,6 +85,8 @@ Documentation must move with code. Do not finish a package change while docs des
84
85
  - Keep `doc.mdx` aligned with the README and package title when packages are added, renamed, or reorganized.
85
86
  - Add or update JSDoc for public exports when behavior, parameters, return values, errors, side effects, or usage expectations need explanation.
86
87
  - For component packages, include visual documentation, stories, or examples for supported states, variants, and interaction states.
88
+ - Keep reusable components as dumb as practical: push data shaping, stateful orchestration, and demo-specific logic into the entry point or owning composition layer.
89
+ - Store demo data in stories instead of embedding it in reusable component source.
87
90
  - Avoid broad architecture essays in package READMEs; keep architectural direction in root-level docs unless it is necessary to use the package.
88
91
 
89
92
  ## Tests Are Required
@@ -94,9 +97,11 @@ Tests should protect public behavior and meaningful regression risk.
94
97
  - Add a real test immediately for new packages. If behavior is not finalized, add a valid placeholder test with a clear public API expectation.
95
98
  - Prefer behavior-focused tests over private implementation lock-in.
96
99
  - Keep tests deterministic and avoid real network, timers, browser, filesystem, or cloud dependencies unless that dependency is the behavior under test.
100
+ - Mock external dependencies from `node_modules` in unit tests so package behavior stays deterministic and local.
97
101
  - When mocking modules, install mocks before loading the module under test.
98
102
  - Use `@jest/globals` in Jest tests.
99
- - Use `--coverage=false` for focused behavioral regression runs when global coverage thresholds would obscure the result.
103
+ - Keep coverage enabled for normal validation. The shared Jest config in `packages/jest/index.ts` requires 100% global coverage for branches, functions, lines, and statements.
104
+ - For small changes, run Jest against the changed tests or changed package with coverage enabled. For larger package changes, public API changes, or cross-package changes, run the full Jest suite with coverage.
100
105
 
101
106
  Typical package test naming:
102
107
 
@@ -127,10 +132,12 @@ Preferred validation commands:
127
132
  yarn tsc --pretty false
128
133
  yarn eslint <changed files or package>
129
134
  yarn prettier --check <changed files or package>
130
- yarn jest <changed tests or package> --runInBand --coverage=false
135
+ yarn jest <changed tests or package> --runInBand
131
136
  ```
132
137
 
133
- Also run `yarn build:dist` for package export, manifest, build-shape, or generated-dist behavior changes. After larger package or library changes, prefer building the affected library/package as an additional confidence check.
138
+ Run ESLint after YAML/YML changes too, for example `yarn eslint <changed.yml>`, because the repo ESLint setup validates YAML files as well.
139
+
140
+ Also run `yarn build:dist` for package export, manifest, build-shape, or generated-dist behavior changes. After larger package or library changes, run the full Jest suite with coverage and prefer building the affected library/package as an additional confidence check.
134
141
 
135
142
  If a required validation command cannot be run, state why and report the remaining risk.
136
143
 
package/README.md CHANGED
@@ -1,105 +1,55 @@
1
1
  # @vyriy/ssg
2
2
 
3
- Part of [Vyriy](https://vyriy.dev) - a calm architecture toolkit for TypeScript, React, SSR, SSG, APIs, and cloud-ready apps.
3
+ Static generation helpers for `vyriy.dev`.
4
4
 
5
- Full documentation: https://vyriy.dev/docs/ssg/
6
-
7
- Static Markdown site generator for Vyriy-style content sites.
8
-
9
- ## Purpose
10
-
11
- This package builds a static site from Markdown `README.md` files. It is intended for content-first sites that need HTML pages, section catalogs, MiniSearch data, featured home content, related links, sitemap, robots, and copied public assets without adopting a full framework.
12
-
13
- Markdown rendering uses `react-markdown` with GitHub-flavored Markdown and `rehype-highlight`, so content supports tables, task lists, autolinks, and highlighted fenced code blocks.
14
-
15
- ## CLI
16
-
17
- Build a site from `site` into `dist`:
18
-
19
- ```bash
20
- vyriy-ssg site --output dist
21
- ```
22
-
23
- The package also exposes the short `ssg` command:
24
-
25
- ```bash
26
- ssg site -o dist
27
- ```
28
-
29
- Useful options:
30
-
31
- - `--site-url <url>` sets canonical URLs and sitemap locations.
32
- - `--site-name <name>` sets the built-in theme name.
33
- - `--stylesheet <href>` links a stylesheet instead of using the built-in CSS.
34
- - `--stylesheet-file <path>` inlines a stylesheet file.
35
- - `--ga <id>` adds a Google Analytics measurement ID.
36
-
37
- ## Content
38
-
39
- By default, the generator expects this shape:
40
-
41
- ```txt
42
- site/
43
- home/README.md
44
- consulting/README.md
45
- docs/README.md
46
- blog/<slug>/README.md
47
- docs/<slug>/README.md
48
- examples/<slug>/README.md
49
- public/
50
- ```
51
-
52
- Markdown files can include simple frontmatter:
53
-
54
- ```md
55
- ---
56
- title: Calm deployment
57
- description: Deployment notes for calm static sites.
58
- date: 2026-06-16
59
- published: true
60
- homePage: true
61
- tags:
62
- - ssg
63
- - deployment
64
- ---
65
-
66
- # Calm deployment
67
-
68
- Page content.
69
- ```
70
-
71
- ## API
5
+ ## Usage
72
6
 
73
7
  ```ts
74
8
  import { buildStaticSite } from '@vyriy/ssg';
75
9
 
76
- await buildStaticSite({
77
- contentPath: 'site',
78
- outputPath: 'dist',
79
- siteUrl: 'https://vyriy.dev',
80
- });
81
- ```
82
-
83
- Custom sections are supported:
84
-
85
- ```ts
86
- await buildStaticSite({
87
- sections: [
88
- {
89
- path: 'articles',
90
- title: 'Articles',
91
- },
92
- ],
93
- });
10
+ await buildStaticSite();
94
11
  ```
95
12
 
96
- Set `index: false` when a section should generate individual pages and search data without its own paginated catalog.
13
+ By default, `buildStaticSite` renders:
14
+
15
+ - `site/home/README.md` to `dist/index.html`
16
+ - `site/docs/README.md` to `dist/docs/index.html`
17
+ - `site/docs/**/README.md` to `dist/docs/**/index.html`
18
+ - `site/blog/**/README.md` to `dist/blog/**/index.html`
19
+ - `site/examples/**/README.md` to `dist/examples/**/index.html`
20
+ - generated docs, blog, and example entries to sibling `.md` files such as
21
+ `dist/docs/ssg.md`, `dist/blog/post.md`, and `dist/examples/demo.md`
22
+ - `site/consulting/README.md` to `dist/consulting/index.html`
23
+ - `dist/blog/index.html` and `dist/examples/index.html` catalog pages, plus
24
+ numbered catalog pages when pagination is needed
25
+ - `dist/404.html`
26
+ - `dist/sitemap.xml`, `dist/robots.txt`, and `dist/llms.txt`
27
+
28
+ When the script is executed from `dist`, it reads content from the parent project
29
+ directory and writes into the current `dist` directory. Blog, documentation, and
30
+ example entries with `published: false` are skipped.
31
+
32
+ Vyriy SSG can emit `.md` versions of generated content pages next to HTML
33
+ pages. This gives agents, LLM tools, and crawlers a clean text representation of
34
+ docs, blog posts, and examples while preserving the normal human-facing HTML
35
+ site. Generated content HTML pages also include a
36
+ `<link rel="alternate" type="text/markdown">` tag that points to the Markdown
37
+ artifact. The generated home page includes lightweight discovery `<link>` tags
38
+ for `/llms.txt`, `/docs/`, and `/sitemap.xml`.
97
39
 
98
40
  ## Exports
99
41
 
100
- - `buildStaticSite(options)` builds the static site.
101
- - `runSsgCli(args)` runs the CLI programmatically.
102
- - `parsePage(markdown)` parses page frontmatter and fallback metadata.
103
- - `renderMarkdown(markdown)` renders Markdown through `react-markdown`, `remark-gfm`, and `rehype-highlight`.
104
- - `renderSitemap(urls, siteUrl)` renders sitemap XML.
105
- - `renderRobotsTxt(siteUrl)` renders robots.txt.
42
+ - `buildStaticSite` builds the static home page, documentation page, consulting
43
+ page, 404 page, blog pages, example pages, and section index pages.
44
+ - `renderRobotsTxt` and `writeRobotsTxt` create a crawler policy that allows all
45
+ indexing, points to the sitemap, and emits permissive content signals for
46
+ search and AI usage.
47
+ - `renderLlmTxt` and `writeLlmTxt` create a readable LLM index for main pages
48
+ and published content sections.
49
+ - `getWebPageJsonLd` and `renderJsonLdScript` create safe WebPage JSON-LD for
50
+ rendered HTML documents.
51
+ - `renderMarkdownPage`, `writeMarkdownPage`, and `getMarkdownOutputPath` create
52
+ agent-readable Markdown artifacts for generated content pages.
53
+ - `BuildStaticSiteOptions` configures source, output, stylesheet, Google
54
+ Analytics measurement ID, and current working directory paths.
55
+ - `PageData` describes parsed Markdown page content and metadata.
@@ -0,0 +1,191 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { getHomePageFeaturedContent, getRelatedDocumentsMap, getSiteSearchDocuments, writeContentData, } from './content-data.js';
4
+ import { buildContentEntries, buildContentSection, writeContentEntryDocuments } from './content-section.js';
5
+ import { writeLlmTxt } from './llm.js';
6
+ import { parsePage } from './parse-page.js';
7
+ import { getStaticSitePaths } from './paths.js';
8
+ import { renderNotFoundPage, renderPage, renderSearchPage } from './render-page.js';
9
+ import { writeRobotsTxt } from './robots.js';
10
+ import { writeSitemap } from './sitemap.js';
11
+ const homeSocialMetadata = {
12
+ description: 'Calm architecture for cloud-ready applications. Small explicit pieces, clear boundaries, predictable behavior.',
13
+ imageAlt: 'Vyriy calm architecture diagram with modular cloud-ready software blocks.',
14
+ imagePath: '/assets/vyriy-calm-architecture.png',
15
+ siteName: 'Vyriy',
16
+ title: 'Vyriy - Calm Architecture',
17
+ };
18
+ export const buildStaticSite = async (options = {}) => {
19
+ const paths = getStaticSitePaths(options.cwd ?? process.cwd());
20
+ const sourcePath = options.sourcePath ?? paths.sourcePath;
21
+ const outputPath = options.outputPath ?? paths.outputPath;
22
+ const page = parsePage(await readFile(sourcePath, 'utf8'));
23
+ const consultingPage = parsePage(await readFile(paths.consultingSourcePath, 'utf8'));
24
+ const docPage = parsePage(await readFile(paths.docSourcePath, 'utf8'));
25
+ const consultingDocument = renderPage(consultingPage, {
26
+ canonicalPath: '/consulting/',
27
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
28
+ siteUrl: options.siteUrl,
29
+ stylesheetContent: options.stylesheetContent,
30
+ stylesheetHref: options.stylesheetHref,
31
+ });
32
+ const docDocument = renderPage(docPage, {
33
+ canonicalPath: '/docs/',
34
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
35
+ siteUrl: options.siteUrl,
36
+ stylesheetContent: options.stylesheetContent,
37
+ stylesheetHref: options.stylesheetHref,
38
+ });
39
+ const notFoundDocument = renderNotFoundPage({
40
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
41
+ siteUrl: options.siteUrl,
42
+ stylesheetContent: options.stylesheetContent,
43
+ stylesheetHref: options.stylesheetHref,
44
+ });
45
+ const searchDocument = renderSearchPage({
46
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
47
+ siteUrl: options.siteUrl,
48
+ stylesheetContent: options.stylesheetContent,
49
+ stylesheetHref: options.stylesheetHref,
50
+ });
51
+ const searchOutputPath = join(paths.outputDirectory, 'search/index.html');
52
+ await mkdir(dirname(outputPath), {
53
+ recursive: true,
54
+ });
55
+ await mkdir(dirname(paths.consultingOutputPath), {
56
+ recursive: true,
57
+ });
58
+ await mkdir(dirname(paths.docOutputPath), {
59
+ recursive: true,
60
+ });
61
+ await mkdir(paths.outputDirectory, {
62
+ recursive: true,
63
+ });
64
+ await mkdir(dirname(searchOutputPath), {
65
+ recursive: true,
66
+ });
67
+ await writeFile(paths.consultingOutputPath, consultingDocument);
68
+ await writeFile(paths.docOutputPath, docDocument);
69
+ await writeFile(join(paths.outputDirectory, '404.html'), notFoundDocument);
70
+ await writeFile(searchOutputPath, searchDocument);
71
+ const blogSection = await buildContentSection('blog', paths.projectRoot, paths.outputDirectory, options.stylesheetHref, options.siteUrl, options.stylesheetContent, options.googleAnalyticsMeasurementId);
72
+ const examplesSection = await buildContentSection('examples', paths.projectRoot, paths.outputDirectory, options.stylesheetHref, options.siteUrl, options.stylesheetContent, options.googleAnalyticsMeasurementId);
73
+ const docsEntries = await buildContentEntries('docs', paths.projectRoot);
74
+ const docsRootEntry = {
75
+ ...docPage,
76
+ href: '/docs/',
77
+ slug: '',
78
+ };
79
+ const contentSections = [
80
+ {
81
+ entries: blogSection.entries,
82
+ section: 'blog',
83
+ },
84
+ {
85
+ entries: [
86
+ docsRootEntry,
87
+ ...docsEntries,
88
+ ],
89
+ section: 'docs',
90
+ },
91
+ {
92
+ entries: examplesSection.entries,
93
+ section: 'examples',
94
+ },
95
+ ];
96
+ const relatedDocuments = getRelatedDocumentsMap(getSiteSearchDocuments(contentSections));
97
+ const featured = getHomePageFeaturedContent(contentSections);
98
+ const document = renderPage(page, {
99
+ canonicalPath: '/',
100
+ discoveryLinks: [
101
+ {
102
+ href: '/llms.txt',
103
+ rel: 'describedby',
104
+ type: 'text/markdown',
105
+ },
106
+ {
107
+ href: '/docs/',
108
+ rel: 'service-doc',
109
+ type: 'text/html',
110
+ },
111
+ {
112
+ href: '/sitemap.xml',
113
+ rel: 'sitemap',
114
+ type: 'application/xml',
115
+ },
116
+ ],
117
+ featured,
118
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
119
+ siteUrl: options.siteUrl,
120
+ socialMetadata: homeSocialMetadata,
121
+ stylesheetContent: options.stylesheetContent,
122
+ stylesheetHref: options.stylesheetHref,
123
+ });
124
+ await writeFile(outputPath, document);
125
+ await Promise.all([
126
+ writeContentEntryDocuments('blog', blogSection.entries, paths.outputDirectory, {
127
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
128
+ relatedDocuments,
129
+ siteUrl: options.siteUrl,
130
+ stylesheetContent: options.stylesheetContent,
131
+ stylesheetHref: options.stylesheetHref,
132
+ }),
133
+ writeContentEntryDocuments('examples', examplesSection.entries, paths.outputDirectory, {
134
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
135
+ relatedDocuments,
136
+ siteUrl: options.siteUrl,
137
+ stylesheetContent: options.stylesheetContent,
138
+ stylesheetHref: options.stylesheetHref,
139
+ }),
140
+ writeContentEntryDocuments('docs', docsEntries, paths.outputDirectory, {
141
+ googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
142
+ relatedDocuments,
143
+ siteUrl: options.siteUrl,
144
+ stylesheetContent: options.stylesheetContent,
145
+ stylesheetHref: options.stylesheetHref,
146
+ }),
147
+ ]);
148
+ await writeContentData(contentSections, paths.outputDirectory);
149
+ await writeSitemap(paths.outputDirectory, [
150
+ {
151
+ path: '/',
152
+ },
153
+ {
154
+ path: '/consulting/',
155
+ },
156
+ {
157
+ path: '/docs/',
158
+ },
159
+ ...docsEntries.map((entry) => ({
160
+ path: entry.href,
161
+ })),
162
+ ...blogSection.indexPaths.map((path) => ({
163
+ path,
164
+ })),
165
+ ...blogSection.entries.map((entry) => ({
166
+ path: entry.href,
167
+ })),
168
+ ...examplesSection.indexPaths.map((path) => ({
169
+ path,
170
+ })),
171
+ ...examplesSection.entries.map((entry) => ({
172
+ path: entry.href,
173
+ })),
174
+ ], options.siteUrl);
175
+ await writeRobotsTxt(paths.outputDirectory, options.siteUrl);
176
+ await writeLlmTxt(paths.outputDirectory, {
177
+ pages: [
178
+ {
179
+ description: page.description,
180
+ path: '/',
181
+ title: page.title,
182
+ },
183
+ {
184
+ description: consultingPage.description,
185
+ path: '/consulting/',
186
+ title: consultingPage.title,
187
+ },
188
+ ],
189
+ sections: contentSections,
190
+ }, options.siteUrl);
191
+ };
@@ -0,0 +1,40 @@
1
+ import type { ComponentProps, FC, ReactNode } from 'react';
2
+ type CardProps = ComponentProps<'article'> & {
3
+ readonly date?: string;
4
+ readonly description: string;
5
+ readonly href: string;
6
+ readonly tags?: readonly string[];
7
+ readonly title: string;
8
+ };
9
+ type CatalogProps = {
10
+ readonly content: ReactNode;
11
+ readonly paginate: {
12
+ readonly getHref?: (page: number) => string;
13
+ readonly page: number;
14
+ readonly pages: number;
15
+ };
16
+ };
17
+ type PageRelatedItem = {
18
+ readonly description: string;
19
+ readonly href: string;
20
+ readonly title: string;
21
+ };
22
+ type PageProps = {
23
+ readonly content: ReactNode;
24
+ readonly featured?: readonly PageRelatedItem[];
25
+ readonly related?: readonly PageRelatedItem[];
26
+ readonly tags?: readonly string[];
27
+ };
28
+ type SearchPageProps = {
29
+ readonly documentsUrl?: string;
30
+ readonly indexUrl?: string;
31
+ readonly miniSearchScriptUrl?: string;
32
+ };
33
+ export declare const Card: FC<CardProps>;
34
+ export declare const Page: FC<PageProps>;
35
+ export declare const Catalog: FC<CatalogProps>;
36
+ export declare const NotFoundPage: FC<{
37
+ readonly homeHref?: string;
38
+ }>;
39
+ export declare const SearchPage: FC<SearchPageProps>;
40
+ export {};