@vyriy/ssg 0.9.1 → 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
@@ -85,6 +85,8 @@ Documentation must move with code. Do not finish a package change while docs des
85
85
  - Keep `doc.mdx` aligned with the README and package title when packages are added, renamed, or reorganized.
86
86
  - Add or update JSDoc for public exports when behavior, parameters, return values, errors, side effects, or usage expectations need explanation.
87
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.
88
90
  - Avoid broad architecture essays in package READMEs; keep architectural direction in root-level docs unless it is necessary to use the package.
89
91
 
90
92
  ## Tests Are Required
@@ -133,6 +135,8 @@ yarn prettier --check <changed files or package>
133
135
  yarn jest <changed tests or package> --runInBand
134
136
  ```
135
137
 
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
+
136
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.
137
141
 
138
142
  If a required validation command cannot be run, state why and report the remaining risk.
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 {};