@vyriy/ssg 0.8.7
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 +102 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/bin/index.js +3 -0
- package/cli.d.ts +7 -0
- package/cli.js +84 -0
- package/content.d.ts +43 -0
- package/content.js +237 -0
- package/html.d.ts +29 -0
- package/html.js +228 -0
- package/index.d.ts +8 -0
- package/index.js +7 -0
- package/markdown.d.ts +3 -0
- package/markdown.js +31 -0
- package/package.json +136 -0
- package/parse-page.d.ts +2 -0
- package/parse-page.js +125 -0
- package/plain.d.ts +4 -0
- package/plain.js +73 -0
- package/robots.d.ts +2 -0
- package/robots.js +19 -0
- package/sitemap.d.ts +4 -0
- package/sitemap.js +38 -0
- package/ssg.d.ts +2 -0
- package/ssg.js +199 -0
- package/types.d.ts +85 -0
package/sitemap.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const defaultSiteUrl = 'https://vyriy.dev';
|
|
4
|
+
const xmlCharacterPattern = /[&<>"']/gu;
|
|
5
|
+
const xmlCharacterEntities = {
|
|
6
|
+
'&': '&',
|
|
7
|
+
'"': '"',
|
|
8
|
+
"'": ''',
|
|
9
|
+
'<': '<',
|
|
10
|
+
'>': '>',
|
|
11
|
+
};
|
|
12
|
+
const escapeXml = (value) => {
|
|
13
|
+
return value.replaceAll(xmlCharacterPattern, (character) => xmlCharacterEntities[character]);
|
|
14
|
+
};
|
|
15
|
+
const normalizeSiteUrl = (siteUrl) => (siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl);
|
|
16
|
+
const normalizePath = (path) => (path.startsWith('/') ? path : `/${path}`);
|
|
17
|
+
export const getAbsoluteUrl = (path, siteUrl = defaultSiteUrl) => {
|
|
18
|
+
return `${normalizeSiteUrl(siteUrl)}${normalizePath(path)}`;
|
|
19
|
+
};
|
|
20
|
+
export const renderSitemap = (urls, siteUrl = defaultSiteUrl) => {
|
|
21
|
+
const urlEntries = urls
|
|
22
|
+
.map((url) => ` <url>\n <loc>${escapeXml(getAbsoluteUrl(url.path, siteUrl))}</loc>\n </url>`)
|
|
23
|
+
.join('\n');
|
|
24
|
+
return [
|
|
25
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
26
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
27
|
+
urlEntries,
|
|
28
|
+
'</urlset>',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
};
|
|
32
|
+
export const writeSitemap = async (outputDirectory, urls, siteUrl = defaultSiteUrl) => {
|
|
33
|
+
const outputPath = join(outputDirectory, 'sitemap.xml');
|
|
34
|
+
await mkdir(dirname(outputPath), {
|
|
35
|
+
recursive: true,
|
|
36
|
+
});
|
|
37
|
+
await writeFile(outputPath, renderSitemap(urls, siteUrl));
|
|
38
|
+
};
|
package/ssg.d.ts
ADDED
package/ssg.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { buildContentEntries, buildContentSection, getHomePageFeaturedContent, getRelatedDocumentsMap, getSiteSearchDocuments, writeContentData, writeContentEntryDocuments, } from './content.js';
|
|
5
|
+
import { defaultStylesheet, renderNotFoundPage, renderPage, renderSearchPage, searchScript } from './html.js';
|
|
6
|
+
import { parsePage } from './parse-page.js';
|
|
7
|
+
import { writeRobotsTxt } from './robots.js';
|
|
8
|
+
import { writeSitemap } from './sitemap.js';
|
|
9
|
+
const defaultSiteName = 'Vyriy';
|
|
10
|
+
const defaultContentPath = 'site';
|
|
11
|
+
const defaultOutputPath = 'dist';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const miniSearchBrowserPath = join(dirname(dirname(require.resolve('minisearch'))), 'umd/index.js');
|
|
14
|
+
const defaultSections = [
|
|
15
|
+
{
|
|
16
|
+
path: 'blog',
|
|
17
|
+
title: 'Blog',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
index: false,
|
|
21
|
+
path: 'docs',
|
|
22
|
+
title: 'Documentation',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
path: 'examples',
|
|
26
|
+
title: 'Examples',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const isNodeError = (error) => {
|
|
30
|
+
return typeof error === 'object' && error !== null && 'code' in error;
|
|
31
|
+
};
|
|
32
|
+
const pathExists = async (path) => {
|
|
33
|
+
try {
|
|
34
|
+
await readdir(path);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const copyDirectory = async (sourcePath, outputPath) => {
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = await readdir(sourcePath, {
|
|
48
|
+
withFileTypes: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
await mkdir(outputPath, {
|
|
58
|
+
recursive: true,
|
|
59
|
+
});
|
|
60
|
+
await Promise.all(entries.map(async (entry) => {
|
|
61
|
+
const sourceEntryPath = join(sourcePath, entry.name);
|
|
62
|
+
const outputEntryPath = join(outputPath, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
await copyDirectory(sourceEntryPath, outputEntryPath);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (entry.isFile()) {
|
|
68
|
+
await mkdir(dirname(outputEntryPath), {
|
|
69
|
+
recursive: true,
|
|
70
|
+
});
|
|
71
|
+
await copyFile(sourceEntryPath, outputEntryPath);
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
};
|
|
75
|
+
const getDefaultPages = (contentPath) => [
|
|
76
|
+
{
|
|
77
|
+
canonicalPath: '/consulting/',
|
|
78
|
+
inputPath: join(contentPath, 'consulting/README.md'),
|
|
79
|
+
outputPath: 'consulting/index.html',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
canonicalPath: '/docs/',
|
|
83
|
+
inputPath: join(contentPath, 'docs/README.md'),
|
|
84
|
+
outputPath: 'docs/index.html',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const renderOptionsFrom = (options) => {
|
|
88
|
+
const siteName = options.siteName ?? defaultSiteName;
|
|
89
|
+
return {
|
|
90
|
+
defaultTitle: options.defaultTitle ?? siteName,
|
|
91
|
+
footerText: options.footerText ?? `Copyright © 2026 ${siteName}`,
|
|
92
|
+
googleAnalyticsMeasurementId: options.googleAnalyticsMeasurementId,
|
|
93
|
+
siteName,
|
|
94
|
+
siteUrl: options.siteUrl,
|
|
95
|
+
stylesheetContent: options.stylesheetContent,
|
|
96
|
+
stylesheetHref: options.stylesheetHref ?? (options.stylesheetContent ? undefined : '/styles.css'),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const writeStandalonePage = async (page, outputDirectory, renderOptions) => {
|
|
100
|
+
try {
|
|
101
|
+
const pageData = parsePage(await readFile(page.inputPath, 'utf8'), renderOptions.defaultTitle);
|
|
102
|
+
if (!pageData.published) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
await mkdir(dirname(join(outputDirectory, page.outputPath)), {
|
|
106
|
+
recursive: true,
|
|
107
|
+
});
|
|
108
|
+
await writeFile(join(outputDirectory, page.outputPath), renderPage(pageData, {
|
|
109
|
+
...renderOptions,
|
|
110
|
+
canonicalPath: page.canonicalPath,
|
|
111
|
+
}));
|
|
112
|
+
return page.canonicalPath;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
export const buildStaticSite = async (options = {}) => {
|
|
122
|
+
const cwd = options.cwd ?? process.cwd();
|
|
123
|
+
const contentPath = resolve(cwd, options.contentPath ?? defaultContentPath);
|
|
124
|
+
const outputDirectory = resolve(cwd, options.outputPath ?? defaultOutputPath);
|
|
125
|
+
const publicPath = resolve(cwd, options.publicPath ?? join(options.contentPath ?? defaultContentPath, 'public'));
|
|
126
|
+
const homePage = options.homePage ?? {
|
|
127
|
+
canonicalPath: '/',
|
|
128
|
+
inputPath: join(contentPath, 'home/README.md'),
|
|
129
|
+
outputPath: 'index.html',
|
|
130
|
+
};
|
|
131
|
+
const pages = options.pages ?? getDefaultPages(contentPath);
|
|
132
|
+
const sections = options.sections ?? defaultSections;
|
|
133
|
+
const renderOptions = renderOptionsFrom(options);
|
|
134
|
+
await mkdir(outputDirectory, {
|
|
135
|
+
recursive: true,
|
|
136
|
+
});
|
|
137
|
+
if (!options.stylesheetContent && !options.stylesheetHref) {
|
|
138
|
+
await writeFile(join(outputDirectory, 'styles.css'), `${defaultStylesheet.trim()}\n`);
|
|
139
|
+
}
|
|
140
|
+
await mkdir(join(outputDirectory, 'assets'), {
|
|
141
|
+
recursive: true,
|
|
142
|
+
});
|
|
143
|
+
await Promise.all([
|
|
144
|
+
copyFile(miniSearchBrowserPath, join(outputDirectory, 'assets/minisearch.js')),
|
|
145
|
+
writeFile(join(outputDirectory, 'assets/search.js'), `${searchScript.trim()}\n`),
|
|
146
|
+
]);
|
|
147
|
+
const sectionResults = await Promise.all(sections.map(async (section) => {
|
|
148
|
+
if (section.index === false) {
|
|
149
|
+
return {
|
|
150
|
+
result: {
|
|
151
|
+
entries: await buildContentEntries(section, contentPath, renderOptions.defaultTitle),
|
|
152
|
+
indexPaths: [],
|
|
153
|
+
},
|
|
154
|
+
section,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
result: await buildContentSection(section, contentPath, outputDirectory, renderOptions),
|
|
159
|
+
section,
|
|
160
|
+
};
|
|
161
|
+
}));
|
|
162
|
+
const contentSections = sectionResults.map(({ result, section }) => ({
|
|
163
|
+
entries: result.entries,
|
|
164
|
+
section: section.path,
|
|
165
|
+
}));
|
|
166
|
+
const relatedDocuments = getRelatedDocumentsMap(getSiteSearchDocuments(contentSections));
|
|
167
|
+
const featured = getHomePageFeaturedContent(contentSections);
|
|
168
|
+
const standalonePaths = (await Promise.all(pages.map((page) => writeStandalonePage(page, outputDirectory, renderOptions)))).filter((path) => Boolean(path));
|
|
169
|
+
const homePageData = parsePage(await readFile(homePage.inputPath, 'utf8'), renderOptions.defaultTitle);
|
|
170
|
+
await writeFile(join(outputDirectory, homePage.outputPath), renderPage(homePageData, {
|
|
171
|
+
...renderOptions,
|
|
172
|
+
canonicalPath: homePage.canonicalPath,
|
|
173
|
+
featured,
|
|
174
|
+
}));
|
|
175
|
+
await Promise.all(sectionResults.map(({ result, section }) => writeContentEntryDocuments(section, result.entries, outputDirectory, {
|
|
176
|
+
...renderOptions,
|
|
177
|
+
relatedDocuments,
|
|
178
|
+
})));
|
|
179
|
+
await writeFile(join(outputDirectory, '404.html'), renderNotFoundPage(renderOptions));
|
|
180
|
+
await mkdir(join(outputDirectory, 'search'), {
|
|
181
|
+
recursive: true,
|
|
182
|
+
});
|
|
183
|
+
await writeFile(join(outputDirectory, 'search/index.html'), renderSearchPage(renderOptions));
|
|
184
|
+
await writeContentData(contentSections, outputDirectory);
|
|
185
|
+
if (options.copyPublic ?? (await pathExists(publicPath))) {
|
|
186
|
+
await copyDirectory(publicPath, outputDirectory);
|
|
187
|
+
}
|
|
188
|
+
await writeSitemap(outputDirectory, [
|
|
189
|
+
{
|
|
190
|
+
path: homePage.canonicalPath ?? '/',
|
|
191
|
+
},
|
|
192
|
+
...standalonePaths.map((path) => ({
|
|
193
|
+
path,
|
|
194
|
+
})),
|
|
195
|
+
...sectionResults.flatMap(({ result }) => result.indexPaths.map((path) => ({ path }))),
|
|
196
|
+
...sectionResults.flatMap(({ result }) => result.entries.map((entry) => ({ path: entry.href }))),
|
|
197
|
+
], options.siteUrl);
|
|
198
|
+
await writeRobotsTxt(outputDirectory, options.siteUrl);
|
|
199
|
+
};
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type PageData = {
|
|
2
|
+
readonly content: string;
|
|
3
|
+
readonly date: string;
|
|
4
|
+
readonly description: string;
|
|
5
|
+
readonly featured?: boolean;
|
|
6
|
+
readonly homePage?: boolean;
|
|
7
|
+
readonly homePageOrder?: number;
|
|
8
|
+
readonly published: boolean;
|
|
9
|
+
readonly tags: readonly string[];
|
|
10
|
+
readonly title: string;
|
|
11
|
+
readonly updatedAt?: string;
|
|
12
|
+
};
|
|
13
|
+
export type StaticSitePage = {
|
|
14
|
+
readonly canonicalPath?: string;
|
|
15
|
+
readonly inputPath: string;
|
|
16
|
+
readonly outputPath: string;
|
|
17
|
+
};
|
|
18
|
+
export type StaticSiteSection = {
|
|
19
|
+
readonly description?: string;
|
|
20
|
+
readonly index?: boolean;
|
|
21
|
+
readonly pageSize?: number;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly title: string;
|
|
24
|
+
};
|
|
25
|
+
export type BuildStaticSiteOptions = {
|
|
26
|
+
readonly contentPath?: string;
|
|
27
|
+
readonly copyPublic?: boolean;
|
|
28
|
+
readonly cwd?: string;
|
|
29
|
+
readonly defaultTitle?: string;
|
|
30
|
+
readonly footerText?: string;
|
|
31
|
+
readonly googleAnalyticsMeasurementId?: string;
|
|
32
|
+
readonly homePage?: StaticSitePage;
|
|
33
|
+
readonly outputPath?: string;
|
|
34
|
+
readonly pages?: readonly StaticSitePage[];
|
|
35
|
+
readonly publicPath?: string;
|
|
36
|
+
readonly sections?: readonly StaticSiteSection[];
|
|
37
|
+
readonly siteName?: string;
|
|
38
|
+
readonly siteUrl?: string;
|
|
39
|
+
readonly stylesheetContent?: string;
|
|
40
|
+
readonly stylesheetHref?: string;
|
|
41
|
+
};
|
|
42
|
+
export type ContentEntry = PageData & {
|
|
43
|
+
readonly href: string;
|
|
44
|
+
readonly section: string;
|
|
45
|
+
readonly slug: string;
|
|
46
|
+
};
|
|
47
|
+
export type ContentSectionBuildResult = {
|
|
48
|
+
readonly entries: readonly ContentEntry[];
|
|
49
|
+
readonly indexPaths: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
export type SitemapUrl = {
|
|
52
|
+
readonly path: string;
|
|
53
|
+
};
|
|
54
|
+
export type SearchDocument = {
|
|
55
|
+
readonly content: string;
|
|
56
|
+
readonly date?: string;
|
|
57
|
+
readonly description: string;
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly section: string;
|
|
60
|
+
readonly slug: string;
|
|
61
|
+
readonly tags: readonly string[];
|
|
62
|
+
readonly title: string;
|
|
63
|
+
readonly updatedAt?: string;
|
|
64
|
+
readonly url: string;
|
|
65
|
+
};
|
|
66
|
+
export type RelatedDocument = {
|
|
67
|
+
readonly description: string;
|
|
68
|
+
readonly score: number;
|
|
69
|
+
readonly section: string;
|
|
70
|
+
readonly slug: string;
|
|
71
|
+
readonly tags: readonly string[];
|
|
72
|
+
readonly title: string;
|
|
73
|
+
readonly url: string;
|
|
74
|
+
};
|
|
75
|
+
export type RelatedDocumentsMap = Record<string, readonly RelatedDocument[]>;
|
|
76
|
+
export type HomePageFeaturedContentItem = {
|
|
77
|
+
readonly date?: string;
|
|
78
|
+
readonly description: string;
|
|
79
|
+
readonly homePageOrder?: number;
|
|
80
|
+
readonly section: string;
|
|
81
|
+
readonly slug: string;
|
|
82
|
+
readonly tags: readonly string[];
|
|
83
|
+
readonly title: string;
|
|
84
|
+
readonly url: string;
|
|
85
|
+
};
|