@vyriy/ssg 0.9.1 → 0.9.3
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 +4 -0
- package/README.md +43 -93
- package/build-static-site.js +191 -0
- package/components.d.ts +40 -0
- package/components.js +343 -0
- package/content-data.d.ts +69 -0
- package/{content.js → content-data.js} +17 -100
- package/content-section.d.ts +11 -0
- package/content-section.js +104 -0
- package/index.d.ts +12 -8
- package/index.js +8 -7
- package/json-ld.d.ts +11 -0
- package/json-ld.js +36 -0
- package/llm.d.ts +12 -0
- package/llm.js +59 -0
- package/markdown-page.d.ts +5 -0
- package/markdown-page.js +55 -0
- package/minify-html.d.ts +1 -0
- package/minify-html.js +51 -0
- package/package.json +109 -62
- package/parse-page.d.ts +1 -1
- package/parse-page.js +9 -9
- package/paths.d.ts +11 -0
- package/paths.js +16 -0
- package/render-page.d.ts +40 -0
- package/render-page.js +193 -0
- package/robots.js +1 -0
- package/sitemap.js +31 -11
- package/types.d.ts +2 -54
- package/bin/index.js +0 -3
- package/cli.d.ts +0 -7
- package/cli.js +0 -84
- package/content.d.ts +0 -43
- package/html.d.ts +0 -29
- package/html.js +0 -228
- package/markdown.d.ts +0 -3
- package/markdown.js +0 -31
- package/ssg.js +0 -199
- /package/{ssg.d.ts → build-static-site.d.ts} +0 -0
- /package/{plain.d.ts → markdown-plain-text.d.ts} +0 -0
- /package/{plain.js → markdown-plain-text.js} +0 -0
package/components.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
const cn = (...classes) => classes.filter(Boolean).join(' ');
|
|
4
|
+
const getTagHref = (tag) => `/search/?tag=${encodeURIComponent(tag)}`;
|
|
5
|
+
const visiblePaginationPages = 3;
|
|
6
|
+
const getPaginationItems = (page, pages) => {
|
|
7
|
+
if (pages <= visiblePaginationPages + 2) {
|
|
8
|
+
return Array.from({ length: pages }, (_value, index) => ({
|
|
9
|
+
page: index + 1,
|
|
10
|
+
type: 'page',
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
const halfWindow = Math.floor(visiblePaginationPages / 2);
|
|
14
|
+
const windowStart = Math.max(2, Math.min(page - halfWindow, pages - visiblePaginationPages));
|
|
15
|
+
const windowEnd = Math.min(pages - 1, windowStart + visiblePaginationPages - 1);
|
|
16
|
+
const items = [
|
|
17
|
+
{
|
|
18
|
+
page: 1,
|
|
19
|
+
type: 'page',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
if (windowStart > 2) {
|
|
23
|
+
items.push({
|
|
24
|
+
key: 'start-ellipsis',
|
|
25
|
+
type: 'ellipsis',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (let itemPage = windowStart; itemPage <= windowEnd; itemPage += 1) {
|
|
29
|
+
items.push({
|
|
30
|
+
page: itemPage,
|
|
31
|
+
type: 'page',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (windowEnd < pages - 1) {
|
|
35
|
+
items.push({
|
|
36
|
+
key: 'end-ellipsis',
|
|
37
|
+
type: 'ellipsis',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
items.push({
|
|
41
|
+
page: pages,
|
|
42
|
+
type: 'page',
|
|
43
|
+
});
|
|
44
|
+
return items;
|
|
45
|
+
};
|
|
46
|
+
const headerSearchScript = String.raw `
|
|
47
|
+
(() => {
|
|
48
|
+
const minimumQueryLength = 2;
|
|
49
|
+
const headers = document.querySelectorAll('.vyriy-header');
|
|
50
|
+
|
|
51
|
+
headers.forEach((header) => {
|
|
52
|
+
if (header.dataset.searchReady === 'true') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
header.dataset.searchReady = 'true';
|
|
57
|
+
|
|
58
|
+
const checkbox = header.querySelector('.vyriy-header__search-checkbox');
|
|
59
|
+
const form = header.querySelector('.vyriy-header__search');
|
|
60
|
+
const input = form ? form.querySelector('input[name="q"]') : null;
|
|
61
|
+
|
|
62
|
+
if (!checkbox || !form || !input) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const focusSearchInput = (attempt = 0) => {
|
|
67
|
+
if (!checkbox.checked) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
input.focus();
|
|
72
|
+
|
|
73
|
+
if (document.activeElement === input || attempt >= 4) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scheduleFocus = window.requestAnimationFrame || window.setTimeout;
|
|
78
|
+
|
|
79
|
+
scheduleFocus(() => focusSearchInput(attempt + 1));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
checkbox.addEventListener('change', () => {
|
|
83
|
+
if (checkbox.checked) {
|
|
84
|
+
focusSearchInput();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
document.addEventListener('keydown', (event) => {
|
|
89
|
+
if (event.key !== 'Escape' || !checkbox.checked) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
checkbox.checked = false;
|
|
94
|
+
input.blur();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
input.addEventListener('input', () => {
|
|
98
|
+
input.setCustomValidity('');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
form.addEventListener('submit', (event) => {
|
|
102
|
+
const query = input.value.trim();
|
|
103
|
+
|
|
104
|
+
if (query.length >= minimumQueryLength) {
|
|
105
|
+
input.value = query;
|
|
106
|
+
input.setCustomValidity('');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
input.setCustomValidity('Enter at least ' + minimumQueryLength + ' characters.');
|
|
112
|
+
input.reportValidity();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
})();
|
|
116
|
+
`;
|
|
117
|
+
const searchScript = String.raw `
|
|
118
|
+
(() => {
|
|
119
|
+
const root = document.getElementById('search-root');
|
|
120
|
+
|
|
121
|
+
if (!root) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const documentsUrl = root.dataset.documentsUrl || '/search/documents.json';
|
|
126
|
+
const indexUrl = root.dataset.indexUrl || '/search/minisearch-index.json';
|
|
127
|
+
const parameters = new URLSearchParams(window.location.search);
|
|
128
|
+
const tag = parameters.get('tag');
|
|
129
|
+
const query = (parameters.get('q') || '').trim();
|
|
130
|
+
const formInput = document.querySelector('.vyriy-search-page__form input[name="q"]');
|
|
131
|
+
|
|
132
|
+
if (formInput) {
|
|
133
|
+
formInput.value = query;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!tag && !query) {
|
|
137
|
+
root.innerHTML = '<p class="vyriy-search-page__status">Choose a post tag or enter a search query to see matching articles and examples.</p>';
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const searchOptions = {
|
|
142
|
+
boost: {
|
|
143
|
+
content: 1,
|
|
144
|
+
description: 2,
|
|
145
|
+
tags: 3,
|
|
146
|
+
title: 4
|
|
147
|
+
},
|
|
148
|
+
fuzzy: 0.2,
|
|
149
|
+
prefix: true
|
|
150
|
+
};
|
|
151
|
+
const miniSearchOptions = {
|
|
152
|
+
fields: ['title', 'description', 'tags', 'content'],
|
|
153
|
+
storeFields: ['title', 'description', 'section', 'slug', 'url', 'tags', 'date'],
|
|
154
|
+
searchOptions
|
|
155
|
+
};
|
|
156
|
+
const resultPageSize = 10;
|
|
157
|
+
let currentMatches = [];
|
|
158
|
+
let visibleResultCount = resultPageSize;
|
|
159
|
+
const normalizeTag = (value) => value.trim().toLowerCase();
|
|
160
|
+
const escapeHtml = (value) => String(value)
|
|
161
|
+
.replaceAll('&', '&')
|
|
162
|
+
.replaceAll('<', '<')
|
|
163
|
+
.replaceAll('>', '>')
|
|
164
|
+
.replaceAll('"', '"')
|
|
165
|
+
.replaceAll("'", ''');
|
|
166
|
+
const getDocumentDescription = (document) => document.description || document.content || '';
|
|
167
|
+
const getDocumentTags = (document) => Array.isArray(document.tags) ? document.tags : [];
|
|
168
|
+
const renderTag = (value) => '<a class="vyriy-search-page__tag" href="/search/?tag=' + encodeURIComponent(value) + '">' + escapeHtml(value) + '</a>';
|
|
169
|
+
const renderDocument = (document) => {
|
|
170
|
+
const description = getDocumentDescription(document);
|
|
171
|
+
const tags = getDocumentTags(document);
|
|
172
|
+
const descriptionHtml = description
|
|
173
|
+
? '<p class="vyriy-search-page__result-description">' + escapeHtml(description) + '</p>'
|
|
174
|
+
: '';
|
|
175
|
+
const tagsHtml = tags.length
|
|
176
|
+
? '<div class="vyriy-search-page__tags">' + tags.map(renderTag).join('') + '</div>'
|
|
177
|
+
: '';
|
|
178
|
+
|
|
179
|
+
return '<article class="vyriy-search-page__result">' +
|
|
180
|
+
'<a class="vyriy-search-page__result-link" href="' + escapeHtml(document.url) + '">' +
|
|
181
|
+
'<h2 class="vyriy-search-page__result-title">' + escapeHtml(document.title) + '</h2>' +
|
|
182
|
+
descriptionHtml +
|
|
183
|
+
'</a>' +
|
|
184
|
+
tagsHtml +
|
|
185
|
+
'</article>';
|
|
186
|
+
};
|
|
187
|
+
const getDocumentMap = (documents) => new Map(documents.map((document) => [document.id, document]));
|
|
188
|
+
const getTagMatches = (documents, value) => {
|
|
189
|
+
const normalizedTag = normalizeTag(value);
|
|
190
|
+
|
|
191
|
+
return documents.filter((document) =>
|
|
192
|
+
getDocumentTags(document).some((candidate) => normalizeTag(candidate) === normalizedTag),
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
const getQueryMatches = (documents, indexJson, value) => {
|
|
196
|
+
const MiniSearch = window.MiniSearch;
|
|
197
|
+
|
|
198
|
+
if (!MiniSearch) {
|
|
199
|
+
throw new Error('MiniSearch is unavailable.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const documentsById = getDocumentMap(documents);
|
|
203
|
+
const search = MiniSearch.loadJSON(JSON.stringify(indexJson), miniSearchOptions);
|
|
204
|
+
|
|
205
|
+
return search.search(value).map((result) => documentsById.get(result.id) || result).filter(Boolean);
|
|
206
|
+
};
|
|
207
|
+
const renderResults = (documents, emptyMessage) => {
|
|
208
|
+
if (!documents.length) {
|
|
209
|
+
return '<p class="vyriy-search-page__status">' + emptyMessage + '</p>';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const visibleDocuments = documents.slice(0, visibleResultCount);
|
|
213
|
+
const remainingCount = documents.length - visibleDocuments.length;
|
|
214
|
+
const moreButton = remainingCount > 0
|
|
215
|
+
? '<button class="vyriy-search-page__more" type="button" data-search-more>Show more (' + remainingCount + ')</button>'
|
|
216
|
+
: '';
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
'<div class="vyriy-search-page__results">' + visibleDocuments.map(renderDocument).join('') + '</div>' +
|
|
220
|
+
moreButton
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
const renderStatus = () => {
|
|
224
|
+
if (tag && query) {
|
|
225
|
+
return 'Query: <strong>' + escapeHtml(query) + '</strong> · Tag: <strong>' + escapeHtml(tag) + '</strong>';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (query) {
|
|
229
|
+
return 'Query: <strong>' + escapeHtml(query) + '</strong>';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return 'Tag: <strong>' + escapeHtml(tag) + '</strong>';
|
|
233
|
+
};
|
|
234
|
+
const renderSearchState = (emptyMessage) => {
|
|
235
|
+
root.innerHTML =
|
|
236
|
+
'<p class="vyriy-search-page__status">' + renderStatus() + '</p>' +
|
|
237
|
+
renderResults(currentMatches, emptyMessage);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
root.addEventListener('click', (event) => {
|
|
241
|
+
const target = event.target;
|
|
242
|
+
|
|
243
|
+
if (!target || !target.closest('[data-search-more]')) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
visibleResultCount += resultPageSize;
|
|
248
|
+
renderSearchState(query ? 'No posts match this search yet.' : 'No posts use this tag yet.');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const dataRequests = [fetch(documentsUrl)];
|
|
252
|
+
|
|
253
|
+
if (query) {
|
|
254
|
+
dataRequests.push(fetch(indexUrl));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
Promise.all(dataRequests)
|
|
258
|
+
.then((responses) => {
|
|
259
|
+
for (const response of responses) {
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
throw new Error('Search data is unavailable.');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return Promise.all(responses.map((response, index) => index === 1 ? response.text() : response.json()));
|
|
266
|
+
})
|
|
267
|
+
.then(([documents, indexText]) => {
|
|
268
|
+
const indexJson = indexText ? JSON.parse(indexText) : undefined;
|
|
269
|
+
const queryMatches = query ? getQueryMatches(documents, indexJson, query) : documents;
|
|
270
|
+
currentMatches = tag ? getTagMatches(queryMatches, tag) : queryMatches;
|
|
271
|
+
|
|
272
|
+
document.title = 'Vyriy Search: ' + (query || tag);
|
|
273
|
+
renderSearchState(query ? 'No posts match this search yet.' : 'No posts use this tag yet.');
|
|
274
|
+
})
|
|
275
|
+
.catch(() => {
|
|
276
|
+
root.innerHTML = '<p class="vyriy-search-page__status">Search data could not be loaded.</p>';
|
|
277
|
+
});
|
|
278
|
+
})();
|
|
279
|
+
`;
|
|
280
|
+
const SearchForm = (props) => {
|
|
281
|
+
const { action = '/search/', buttonLabel = 'Search', className, inputLabel = 'Search query', inputName = 'q', method = 'get', minimumQueryLength = 2, placeholder = 'Search', showButton = true, ...rest } = props;
|
|
282
|
+
const inputId = useId();
|
|
283
|
+
return (_jsxs("form", { ...rest, action: action, className: cn('vyriy-search-form', className), method: method, children: [_jsx("label", { className: "vyriy-search-form__label", htmlFor: inputId, children: inputLabel }), _jsx("input", { className: "vyriy-search-form__input", id: inputId, minLength: minimumQueryLength, name: inputName, placeholder: placeholder, required: true, type: "search" }), showButton ? (_jsx("button", { className: "vyriy-search-form__button", type: "submit", children: buttonLabel })) : null] }));
|
|
284
|
+
};
|
|
285
|
+
const Navigation = (props) => {
|
|
286
|
+
const { ariaLabel = 'Main navigation', className, menuLabel = 'Menu', ...rest } = props;
|
|
287
|
+
const menuId = useId();
|
|
288
|
+
const listId = useId();
|
|
289
|
+
return (_jsxs("nav", { ...rest, "aria-label": ariaLabel, className: cn('vyriy-navigation', className), children: [_jsx("input", { "aria-controls": listId, "aria-label": menuLabel, className: "vyriy-navigation__checkbox", id: menuId, type: "checkbox" }), _jsxs("label", { className: "vyriy-navigation__toggle", htmlFor: menuId, children: [_jsxs("span", { className: "vyriy-navigation__toggle-icon", "aria-hidden": "true", children: [_jsx("span", { className: "vyriy-navigation__toggle-line" }), _jsx("span", { className: "vyriy-navigation__toggle-line" }), _jsx("span", { className: "vyriy-navigation__toggle-line" })] }), _jsx("span", { className: "vyriy-navigation__toggle-text", children: menuLabel })] }), _jsxs("ul", { className: "vyriy-navigation__list", id: listId, children: [_jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/blog/", children: "Blog" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/examples/", children: "Examples" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/docs/", children: "Documentation" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "https://github.com/evheniy/vyriy", rel: "noreferrer", target: "_blank", children: "GitHub" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/consulting/", children: "Consulting" }) })] })] }));
|
|
290
|
+
};
|
|
291
|
+
const Header = (props) => {
|
|
292
|
+
const { className, homeHref = '/', logoAlt = '', logoSrc = '/assets/vyriy-v-wings.png', name, ...rest } = props;
|
|
293
|
+
const searchToggleId = useId();
|
|
294
|
+
return (_jsxs("header", { ...rest, className: cn('vyriy-header', className), children: [_jsxs("div", { className: "vyriy-header__inner", children: [_jsxs("a", { className: "vyriy-header__brand", href: homeHref, children: [_jsx("img", { className: "vyriy-header__logo", src: logoSrc, alt: logoAlt, width: "72", height: "46", fetchPriority: "low" }), name] }), _jsxs("div", { className: "vyriy-header__actions", children: [_jsxs("div", { className: "vyriy-header__search-shell", children: [_jsx("input", { className: "vyriy-header__search-checkbox", id: searchToggleId, type: "checkbox" }), _jsxs("label", { className: "vyriy-header__search-toggle", htmlFor: searchToggleId, children: [_jsx("span", { className: "vyriy-header__search-label", children: "Search" }), _jsx("span", { className: "vyriy-header__search-icon", "aria-hidden": "true" })] }), _jsx(SearchForm, { className: "vyriy-header__search", placeholder: "Search", showButton: false })] }), _jsx(Navigation, { className: "vyriy-header__navigation" })] })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: headerSearchScript } })] }));
|
|
295
|
+
};
|
|
296
|
+
const Footer = (props) => {
|
|
297
|
+
const { className, text, ...rest } = props;
|
|
298
|
+
return (_jsx("footer", { ...rest, className: cn('vyriy-footer', className), children: _jsx("div", { className: "vyriy-footer__inner", children: _jsx("p", { className: "vyriy-footer__text", children: text }) }) }));
|
|
299
|
+
};
|
|
300
|
+
const Layout = (props) => {
|
|
301
|
+
const { children, footerText, name } = props;
|
|
302
|
+
return (_jsxs("div", { className: "vyriy-layout", children: [_jsx(Header, { name: name }), _jsx("main", { className: "vyriy-layout__main", children: children }), _jsx(Footer, { text: footerText })] }));
|
|
303
|
+
};
|
|
304
|
+
export const Card = (props) => {
|
|
305
|
+
const { className, date, description, href, tags = [], title, ...rest } = props;
|
|
306
|
+
const hasMetadata = Boolean(date) || tags.length > 0;
|
|
307
|
+
return (_jsx("article", { ...rest, className: cn('vyriy-card', className), children: _jsxs("a", { "aria-label": title, className: "vyriy-card__link", href: href, children: [_jsx("h2", { className: "vyriy-card__title", children: title }), _jsx("p", { className: "vyriy-card__description", children: description }), hasMetadata ? (_jsxs("div", { className: "vyriy-card__meta", children: [date ? (_jsx("time", { className: "vyriy-card__date", dateTime: date, children: date })) : null, tags.length > 0 ? (_jsx("ul", { className: "vyriy-card__tags", "aria-label": "Tags", children: tags.map((tag) => (_jsx("li", { className: "vyriy-card__tag", children: tag }, tag))) })) : null] })) : null] }) }));
|
|
308
|
+
};
|
|
309
|
+
export const Page = (props) => {
|
|
310
|
+
const { content, featured = [], related = [], tags = [] } = props;
|
|
311
|
+
return (_jsx(Layout, { footerText: "Copyright \u00A9 2026 Vyriy", name: "Vyriy", children: _jsxs("section", { className: "vyriy-page", children: [_jsx("div", { className: "vyriy-page__content", children: content }), featured.length > 0 ? (_jsx("section", { "aria-label": "Featured posts", className: "vyriy-page__featured", children: _jsx("div", { className: "vyriy-page__featured-list", children: featured.map((item) => (_jsx(Card, { className: "vyriy-page__featured-card", description: item.description, href: item.href, title: item.title }, item.href))) }) })) : null, tags.length > 0 ? (_jsx("nav", { "aria-label": "Post tags", className: "vyriy-page__tags", children: tags.map((tag) => (_jsx("a", { className: "vyriy-page__tag", href: getTagHref(tag), children: tag }, tag))) })) : null, related.length > 0 ? (_jsxs("aside", { "aria-labelledby": "related-posts-title", className: "vyriy-page__related", children: [_jsx("h2", { className: "vyriy-page__section-title", id: "related-posts-title", children: "Related posts" }), _jsx("div", { className: "vyriy-page__related-list", children: related.map((item) => (_jsx(Card, { className: "vyriy-page__related-card", description: item.description, href: item.href, title: item.title }, item.href))) })] })) : null] }) }));
|
|
312
|
+
};
|
|
313
|
+
const Pagination = (props) => {
|
|
314
|
+
const { className, getHref, page, pages, ...rest } = props;
|
|
315
|
+
if (pages <= 1) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const paginationItems = getPaginationItems(page, pages);
|
|
319
|
+
const renderControl = (label, targetPage, disabled) => {
|
|
320
|
+
if (!getHref || disabled) {
|
|
321
|
+
return (_jsx("button", { className: "vyriy-pagination__control", disabled: disabled, type: "button", children: label }));
|
|
322
|
+
}
|
|
323
|
+
return (_jsx("a", { className: "vyriy-pagination__control", href: getHref(targetPage), children: label }));
|
|
324
|
+
};
|
|
325
|
+
return (_jsxs("nav", { ...rest, "aria-label": "Pagination", className: cn('vyriy-pagination', className), children: [renderControl('Prev', page - 1, page === 1), _jsx("ol", { className: "vyriy-pagination__pages", children: paginationItems.map((item) => {
|
|
326
|
+
if (item.type === 'ellipsis') {
|
|
327
|
+
return (_jsx("li", { className: "vyriy-pagination__item", children: _jsx("span", { className: "vyriy-pagination__ellipsis", "aria-hidden": "true", children: "..." }) }, item.key));
|
|
328
|
+
}
|
|
329
|
+
return (_jsx("li", { className: "vyriy-pagination__item", children: getHref && item.page !== page ? (_jsx("a", { className: "vyriy-pagination__page", href: getHref(item.page), children: item.page })) : (_jsx("button", { "aria-current": item.page === page ? 'page' : undefined, className: "vyriy-pagination__page", disabled: item.page === page, type: "button", children: item.page })) }, item.page));
|
|
330
|
+
}) }), renderControl('Last', pages, page === pages)] }));
|
|
331
|
+
};
|
|
332
|
+
export const Catalog = (props) => {
|
|
333
|
+
const { content, paginate } = props;
|
|
334
|
+
return (_jsx(Layout, { footerText: "Copyright \u00A9 2026 Vyriy", name: "Vyriy", children: _jsxs("section", { className: "vyriy-catalog", children: [_jsx("div", { className: "vyriy-catalog__content", children: content }), _jsx(Pagination, { getHref: paginate.getHref, page: paginate.page, pages: paginate.pages })] }) }));
|
|
335
|
+
};
|
|
336
|
+
export const NotFoundPage = (props) => {
|
|
337
|
+
const { homeHref = '/' } = props;
|
|
338
|
+
return (_jsx(Page, { content: _jsxs(_Fragment, { children: [_jsx("h1", { children: "Page not found" }), _jsx("p", { children: "The page you are looking for does not exist. Return to the home page or use the navigation above." }), _jsx("p", { children: _jsx("a", { href: homeHref, children: "Return home" }) })] }) }));
|
|
339
|
+
};
|
|
340
|
+
export const SearchPage = (props) => {
|
|
341
|
+
const { documentsUrl = '/search/documents.json', indexUrl = '/search/minisearch-index.json', miniSearchScriptUrl = '/assets/minisearch.js', } = props;
|
|
342
|
+
return (_jsx(Page, { content: _jsxs("section", { className: "vyriy-search-page", children: [_jsx("h1", { children: "Search" }), _jsx("p", { children: "Search articles and examples by text query or tag." }), _jsx(SearchForm, { className: "vyriy-search-page__form", placeholder: "Search articles and examples" }), _jsx("div", { "data-documents-url": documentsUrl, "data-index-url": indexUrl, id: "search-root" }), _jsx("script", { src: miniSearchScriptUrl }), _jsx("script", { dangerouslySetInnerHTML: { __html: searchScript } })] }) }));
|
|
343
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ContentEntry, ContentSection } from './types.js';
|
|
2
|
+
export type SearchDocument = {
|
|
3
|
+
readonly content: string;
|
|
4
|
+
readonly date?: string;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly section: ContentSection;
|
|
8
|
+
readonly slug: string;
|
|
9
|
+
readonly tags: readonly string[];
|
|
10
|
+
readonly title: string;
|
|
11
|
+
readonly updatedAt?: string;
|
|
12
|
+
readonly url: string;
|
|
13
|
+
};
|
|
14
|
+
export type RelatedDocument = {
|
|
15
|
+
readonly description: string;
|
|
16
|
+
readonly score: number;
|
|
17
|
+
readonly section: ContentSection;
|
|
18
|
+
readonly slug: string;
|
|
19
|
+
readonly tags: readonly string[];
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly url: string;
|
|
22
|
+
};
|
|
23
|
+
export type RelatedDocumentsMap = Record<string, readonly RelatedDocument[]>;
|
|
24
|
+
export type HomePageFeaturedContentItem = {
|
|
25
|
+
readonly date?: string;
|
|
26
|
+
readonly description: string;
|
|
27
|
+
readonly homePageOrder?: number;
|
|
28
|
+
readonly section: ContentSection;
|
|
29
|
+
readonly slug: string;
|
|
30
|
+
readonly tags: readonly string[];
|
|
31
|
+
readonly title: string;
|
|
32
|
+
readonly url: string;
|
|
33
|
+
};
|
|
34
|
+
export type ContentDataSection = {
|
|
35
|
+
readonly entries: readonly ContentEntry[];
|
|
36
|
+
readonly section: ContentSection;
|
|
37
|
+
};
|
|
38
|
+
export declare const contentSearchOptions: {
|
|
39
|
+
boost: {
|
|
40
|
+
content: number;
|
|
41
|
+
description: number;
|
|
42
|
+
tags: number;
|
|
43
|
+
title: number;
|
|
44
|
+
};
|
|
45
|
+
fuzzy: number;
|
|
46
|
+
prefix: boolean;
|
|
47
|
+
};
|
|
48
|
+
export declare const contentMiniSearchOptions: {
|
|
49
|
+
fields: string[];
|
|
50
|
+
storeFields: string[];
|
|
51
|
+
searchOptions: {
|
|
52
|
+
boost: {
|
|
53
|
+
content: number;
|
|
54
|
+
description: number;
|
|
55
|
+
tags: number;
|
|
56
|
+
title: number;
|
|
57
|
+
};
|
|
58
|
+
fuzzy: number;
|
|
59
|
+
prefix: boolean;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export declare const getPlainTextFromMarkdown: (markdown: string) => string;
|
|
63
|
+
export declare const getContentUrl: (section: ContentSection, slug: string) => string;
|
|
64
|
+
export declare const getSearchDocuments: (section: ContentSection, entries: readonly ContentEntry[]) => readonly SearchDocument[];
|
|
65
|
+
export declare const getSiteSearchDocuments: (sections: readonly ContentDataSection[]) => readonly SearchDocument[];
|
|
66
|
+
export declare const getMiniSearchIndexJson: (documents: readonly SearchDocument[]) => unknown;
|
|
67
|
+
export declare const getRelatedDocumentsMap: (documents: readonly SearchDocument[]) => RelatedDocumentsMap;
|
|
68
|
+
export declare const getHomePageFeaturedContent: (sections: readonly ContentDataSection[]) => readonly HomePageFeaturedContentItem[];
|
|
69
|
+
export declare const writeContentData: (sections: readonly ContentDataSection[], outputDirectory: string) => Promise<void>;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { mkdir,
|
|
2
|
-
import { dirname, join
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import MiniSearch from 'minisearch';
|
|
4
|
-
import {
|
|
5
|
-
import { getPlainTextFromMarkdown } from './markdown.js';
|
|
6
|
-
import { parsePage } from './parse-page.js';
|
|
4
|
+
import { replaceInlineCode, replaceMarkdownLinks, stripFencedCode, stripHtmlTags } from './markdown-plain-text.js';
|
|
7
5
|
const relatedDocumentCount = 4;
|
|
8
6
|
const homePageFeaturedContentCount = 4;
|
|
9
7
|
const minimumRelatedScore = 10;
|
|
@@ -35,101 +33,21 @@ export const contentMiniSearchOptions = {
|
|
|
35
33
|
],
|
|
36
34
|
searchOptions: contentSearchOptions,
|
|
37
35
|
};
|
|
38
|
-
const
|
|
39
|
-
|
|
36
|
+
const markdownSyntaxPattern = /[#>*_~|[\](){}\\-]+/gu;
|
|
37
|
+
const whitespacePattern = /\s+/gu;
|
|
38
|
+
const wordPattern = /[\p{L}\p{N}]+/gu;
|
|
39
|
+
export const getPlainTextFromMarkdown = (markdown) => {
|
|
40
|
+
return stripHtmlTags(replaceInlineCode(replaceMarkdownLinks(stripFencedCode(markdown))))
|
|
41
|
+
.replaceAll(markdownSyntaxPattern, ' ')
|
|
42
|
+
.replaceAll(whitespacePattern, ' ')
|
|
43
|
+
.trim();
|
|
40
44
|
};
|
|
41
|
-
export const
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
entries = await readdir(directory, {
|
|
45
|
-
withFileTypes: true,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
const paths = await Promise.all(entries.map(async (entry) => {
|
|
55
|
-
const entryPath = join(directory, entry.name);
|
|
56
|
-
if (entry.isDirectory()) {
|
|
57
|
-
return findReadmePaths(entryPath);
|
|
58
|
-
}
|
|
59
|
-
return entry.name === 'README.md' ? [entryPath] : [];
|
|
60
|
-
}));
|
|
61
|
-
return paths.flat();
|
|
45
|
+
export const getContentUrl = (section, slug) => {
|
|
46
|
+
return slug ? `/${section}/${slug}/` : `/${section}/`;
|
|
62
47
|
};
|
|
63
|
-
const
|
|
64
|
-
return
|
|
65
|
-
.split(sep)
|
|
66
|
-
.filter((segment) => segment && segment !== '.')
|
|
67
|
-
.join('/');
|
|
48
|
+
const getOptionalDate = (date) => {
|
|
49
|
+
return date || undefined;
|
|
68
50
|
};
|
|
69
|
-
const writeDocument = async (outputPath, document) => {
|
|
70
|
-
await mkdir(dirname(outputPath), {
|
|
71
|
-
recursive: true,
|
|
72
|
-
});
|
|
73
|
-
await writeFile(outputPath, document);
|
|
74
|
-
};
|
|
75
|
-
const getContentIndexHref = (sectionPath, page) => {
|
|
76
|
-
return page <= 1 ? `/${sectionPath}/` : `/${sectionPath}/${page}/`;
|
|
77
|
-
};
|
|
78
|
-
const getContentIndexOutputPath = (outputDirectory, sectionPath, page) => {
|
|
79
|
-
return page <= 1
|
|
80
|
-
? join(outputDirectory, sectionPath, 'index.html')
|
|
81
|
-
: join(outputDirectory, sectionPath, String(page), 'index.html');
|
|
82
|
-
};
|
|
83
|
-
export const buildContentEntries = async (section, contentPath, defaultTitle = 'Vyriy') => {
|
|
84
|
-
const sectionDirectory = join(contentPath, section.path);
|
|
85
|
-
const readmePaths = await findReadmePaths(sectionDirectory);
|
|
86
|
-
const entries = (await Promise.all(readmePaths.map(async (readmePath) => {
|
|
87
|
-
const slug = getSlug(sectionDirectory, readmePath);
|
|
88
|
-
const page = parsePage(await readFile(readmePath, 'utf8'), defaultTitle);
|
|
89
|
-
if (!slug || !page.published) {
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
...page,
|
|
94
|
-
href: `/${section.path}/${slug}/`,
|
|
95
|
-
section: section.path,
|
|
96
|
-
slug,
|
|
97
|
-
};
|
|
98
|
-
})))
|
|
99
|
-
.filter((entry) => Boolean(entry))
|
|
100
|
-
.sort((left, right) => right.date.localeCompare(left.date) || left.title.localeCompare(right.title));
|
|
101
|
-
return entries;
|
|
102
|
-
};
|
|
103
|
-
export const buildContentSection = async (section, contentPath, outputDirectory, renderOptions) => {
|
|
104
|
-
const entries = await buildContentEntries(section, contentPath, renderOptions.defaultTitle);
|
|
105
|
-
const pageSize = section.pageSize ?? 10;
|
|
106
|
-
const pages = Math.max(1, Math.ceil(entries.length / pageSize));
|
|
107
|
-
const indexPaths = Array.from({ length: pages }, (_value, index) => getContentIndexHref(section.path, index + 1));
|
|
108
|
-
await Promise.all(indexPaths.map((_path, index) => {
|
|
109
|
-
const page = index + 1;
|
|
110
|
-
const pageEntries = entries.slice(index * pageSize, page * pageSize);
|
|
111
|
-
return writeDocument(getContentIndexOutputPath(outputDirectory, section.path, page), renderContentIndex(pageEntries, {
|
|
112
|
-
...renderOptions,
|
|
113
|
-
page,
|
|
114
|
-
pages,
|
|
115
|
-
sectionPath: section.path,
|
|
116
|
-
sectionTitle: section.title,
|
|
117
|
-
}));
|
|
118
|
-
}));
|
|
119
|
-
return {
|
|
120
|
-
entries,
|
|
121
|
-
indexPaths,
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
export const writeContentEntryDocuments = async (section, entries, outputDirectory, renderOptions) => {
|
|
125
|
-
await Promise.all(entries.map((entry) => writeDocument(join(outputDirectory, section.path, entry.slug, 'index.html'), renderPage(entry, {
|
|
126
|
-
...renderOptions,
|
|
127
|
-
canonicalPath: entry.href,
|
|
128
|
-
related: renderOptions.relatedDocuments?.[`${section.path}:${entry.slug}`] ?? [],
|
|
129
|
-
showTags: true,
|
|
130
|
-
}))));
|
|
131
|
-
};
|
|
132
|
-
const getOptionalDate = (date) => date || undefined;
|
|
133
51
|
export const getSearchDocuments = (section, entries) => {
|
|
134
52
|
return entries.map((entry) => ({
|
|
135
53
|
content: getPlainTextFromMarkdown(entry.content),
|
|
@@ -141,7 +59,7 @@ export const getSearchDocuments = (section, entries) => {
|
|
|
141
59
|
tags: entry.tags,
|
|
142
60
|
title: entry.title,
|
|
143
61
|
updatedAt: entry.updatedAt,
|
|
144
|
-
url: entry.
|
|
62
|
+
url: getContentUrl(section, entry.slug),
|
|
145
63
|
}));
|
|
146
64
|
};
|
|
147
65
|
export const getSiteSearchDocuments = (sections) => {
|
|
@@ -152,7 +70,6 @@ export const getMiniSearchIndexJson = (documents) => {
|
|
|
152
70
|
miniSearch.addAll([...documents]);
|
|
153
71
|
return miniSearch.toJSON();
|
|
154
72
|
};
|
|
155
|
-
const wordPattern = /[\p{L}\p{N}]+/gu;
|
|
156
73
|
const getKeywords = (text) => {
|
|
157
74
|
return new Set((text.toLowerCase().match(wordPattern) ?? []).map((word) => word.trim()).filter((word) => word.length >= 4));
|
|
158
75
|
};
|
|
@@ -216,7 +133,7 @@ export const getHomePageFeaturedContent = (sections) => {
|
|
|
216
133
|
slug: entry.slug,
|
|
217
134
|
tags: entry.tags,
|
|
218
135
|
title: entry.title,
|
|
219
|
-
url: entry.
|
|
136
|
+
url: getContentUrl(section, entry.slug),
|
|
220
137
|
}))
|
|
221
138
|
.slice(0, homePageFeaturedContentCount);
|
|
222
139
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RelatedDocumentsMap } from './content-data.js';
|
|
2
|
+
import type { ContentEntry, ContentSection, ContentSectionBuildResult } from './types.js';
|
|
3
|
+
export declare const buildContentEntries: (section: ContentSection, projectRoot: string) => Promise<ContentEntry[]>;
|
|
4
|
+
export declare const writeContentEntryDocuments: (section: ContentSection, entries: readonly ContentEntry[], outputDirectory: string, { googleAnalyticsMeasurementId, relatedDocuments, siteUrl, stylesheetContent, stylesheetHref, }?: {
|
|
5
|
+
readonly googleAnalyticsMeasurementId?: string;
|
|
6
|
+
readonly relatedDocuments?: RelatedDocumentsMap;
|
|
7
|
+
readonly siteUrl?: string;
|
|
8
|
+
readonly stylesheetContent?: string;
|
|
9
|
+
readonly stylesheetHref?: string;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
export declare const buildContentSection: (section: ContentSection, projectRoot: string, outputDirectory: string, stylesheetHref?: string, siteUrl?: string, stylesheetContent?: string, googleAnalyticsMeasurementId?: string) => Promise<ContentSectionBuildResult>;
|