@soubiran/vite 0.0.0 → 0.1.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.
@@ -0,0 +1,79 @@
1
+ import { UserConfig } from "vite";
2
+ import { Options } from "unplugin-vue-markdown/types";
3
+
4
+ //#region src/assert.d.ts
5
+ type AssertFn = (id: string, frontmatter: Record<string, any>) => void;
6
+ //#endregion
7
+ //#region src/structured-data/breadcrumb.d.ts
8
+
9
+ interface BreadcrumbItem {
10
+ title: string;
11
+ type?: 'WebSite' | 'WebPage';
12
+ url?: string;
13
+ }
14
+ //#endregion
15
+ //#region src/structured-data/person.d.ts
16
+ interface PersonOptions {
17
+ name: string;
18
+ sameAs: string[];
19
+ }
20
+ //#endregion
21
+ //#region src/structured-data/index.d.ts
22
+ interface StructuredDataPageConfig {
23
+ type: 'article' | 'collection' | 'default';
24
+ breadcrumbItems?: BreadcrumbItem[];
25
+ }
26
+ //#endregion
27
+ //#region vite.config.d.ts
28
+ /**
29
+ * Main configuration interface for the infrastructure status app.
30
+ */
31
+ interface Options$1 {
32
+ /**
33
+ * Extracts the page identifier from a given file path or id.
34
+ * @param id - The file path or identifier.
35
+ * @returns The extracted page name, or null if not found.
36
+ */
37
+ extractPage: (id: string) => string | null;
38
+ /**
39
+ * Markdown rendering options for unplugin-vue-markdown.
40
+ */
41
+ markdown?: Options;
42
+ /**
43
+ * SEO and structured data configuration.
44
+ */
45
+ seo: {
46
+ /**
47
+ * Person information for Schema.org structured data.
48
+ */
49
+ person: PersonOptions;
50
+ /**
51
+ * Custom validation rules for frontmatter fields.
52
+ */
53
+ assert?: {
54
+ /**
55
+ * Validation rules function for frontmatter.
56
+ */
57
+ rules?: AssertFn;
58
+ };
59
+ /**
60
+ * Structured data generation configuration.
61
+ */
62
+ structuredData?: {
63
+ /**
64
+ * Callback to determine page type and configuration for structured data generation.
65
+ * @param page - The page name or null.
66
+ * @param frontmatter - The frontmatter data for the page.
67
+ * @returns Structured data configuration for the page.
68
+ */
69
+ pageConfig?: (page: string | null, frontmatter: Record<string, any>) => StructuredDataPageConfig;
70
+ };
71
+ };
72
+ /**
73
+ * Categories to generate API JSON files for (e.g., ['websites', 'platforms']).
74
+ */
75
+ apiCategories?: string[];
76
+ }
77
+ declare const _default: (title: string, hostname: string, options: Options$1, config?: UserConfig) => Record<string, any>;
78
+ //#endregion
79
+ export { type BreadcrumbItem, type PersonOptions, type StructuredDataPageConfig, _default as default };
@@ -0,0 +1,754 @@
1
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import ui from "@nuxt/ui/vite";
4
+ import soubiranComposablesImports from "@soubiran/ui/imports";
5
+ import soubiranResolver from "@soubiran/ui/resolver";
6
+ import { unheadVueComposablesImports } from "@unhead/vue";
7
+ import vue from "@vitejs/plugin-vue";
8
+ import matter from "gray-matter";
9
+ import fonts from "unplugin-fonts/vite";
10
+ import icons from "unplugin-icons/vite";
11
+ import markdown from "unplugin-vue-markdown/vite";
12
+ import vueRouter from "unplugin-vue-router/vite";
13
+ import { defineConfig, mergeConfig } from "vite";
14
+ import { joinURL, withoutTrailingSlash } from "ufo";
15
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
16
+ import { blurhashToDataUri } from "@unpic/placeholder";
17
+ import MarkdownItGitHubAlerts from "markdown-it-github-alerts";
18
+ import implicitFigures from "markdown-it-image-figures";
19
+ import linkAttributes from "markdown-it-link-attributes";
20
+ import { fromAsyncCodeToHtml } from "@shikijs/markdown-it/async";
21
+ import { codeToHtml } from "shiki";
22
+ import { defaultOptions, findHeadlineElements, flatHeadlineItemsToNestedTree, getTokensText, slugify } from "markdown-it-table-of-contents";
23
+ import { Buffer } from "node:buffer";
24
+ import fs from "fs-extra";
25
+ import sharp from "sharp";
26
+ import { cwd } from "node:process";
27
+ import { cyan, dim, green, yellow } from "ansis";
28
+ import { createHash } from "node:crypto";
29
+ import { SitemapStream } from "sitemap";
30
+
31
+ //#region src/assert.ts
32
+ function createAssert(customAssert) {
33
+ return (id, frontmatter) => {
34
+ if (frontmatter.description) {
35
+ const descLength = frontmatter.description.length;
36
+ if (descLength < 110 || descLength > 160) throw new Error(`Description length must be between 110 and 160 characters. Current length: ${descLength} in file: ${id}`);
37
+ }
38
+ customAssert?.(id, frontmatter);
39
+ };
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/utils.ts
44
+ function getUri(id) {
45
+ return withoutTrailingSlash(id.split("/pages/")[1].replace(/\.md$/, "").replace(/\.vue$/, "").replace(/index$/, ""));
46
+ }
47
+ function toUrl(hostname, ...paths) {
48
+ return joinURL(`https://${hostname}`, ...paths);
49
+ }
50
+
51
+ //#endregion
52
+ //#region src/canonical.ts
53
+ function getCanonicalUrl(id, hostname) {
54
+ return joinURL(toUrl(hostname), getUri(id));
55
+ }
56
+ function canonical(id, frontmatter, hostname) {
57
+ const url = getCanonicalUrl(id, hostname);
58
+ frontmatter.meta ??= [];
59
+ frontmatter.meta.push({
60
+ property: "og:url",
61
+ content: url
62
+ });
63
+ frontmatter.link ??= [];
64
+ frontmatter.link.push({
65
+ rel: "canonical",
66
+ href: url
67
+ });
68
+ }
69
+
70
+ //#endregion
71
+ //#region src/markdown-it/custom-image.ts
72
+ function customImage(md, hostname) {
73
+ md.use((md$1) => {
74
+ const imageRule = md$1.renderer.rules.image;
75
+ md$1.renderer.rules.image = async (tokens, idx, options, env, self) => {
76
+ const token = tokens[idx];
77
+ const src = token.attrGet("src");
78
+ if (src) {
79
+ if (!src.startsWith("http")) {
80
+ const remoteSrc = `https://assets.${hostname}`;
81
+ const metadataFilename = `${src}.json`;
82
+ const cachedMetadataFilename = join(".cache", metadataFilename);
83
+ let metadata = await readFile(cachedMetadataFilename, "utf-8").then((text) => JSON.parse(text)).catch(() => void 0);
84
+ if (!metadata) {
85
+ metadata = await fetch(joinURL(remoteSrc, metadataFilename)).then((res) => res.json());
86
+ await mkdir(dirname(cachedMetadataFilename), { recursive: true });
87
+ await writeFile(cachedMetadataFilename, JSON.stringify(metadata, null, 2));
88
+ }
89
+ token.attrSet("loading", "lazy");
90
+ token.attrSet("width", metadata.width.toString());
91
+ token.attrSet("height", metadata.height.toString());
92
+ token.attrSet("style", `background-size: cover; background-image: url(${blurhashToDataUri(metadata.blurhash)});`);
93
+ token.attrSet("src", joinURL(`https://${hostname}`, "cdn-cgi/image", "width=1200,quality=80,format=auto", remoteSrc, src));
94
+ }
95
+ }
96
+ return imageRule(tokens, idx, options, env, self);
97
+ };
98
+ });
99
+ }
100
+
101
+ //#endregion
102
+ //#region src/markdown-it/custom-link.ts
103
+ function customLink(md, hostname) {
104
+ md.use((md$1) => {
105
+ const linkRule = md$1.renderer.rules.link_open;
106
+ md$1.renderer.rules.link_open = (tokens, idx, options, env, self) => {
107
+ const token = tokens[idx];
108
+ const href = token.attrGet("href");
109
+ if (href && /^https?:\/\/(?:[a-z0-9-]+\.)?soubiran\.dev(?:[/?#]|$)/.test(href)) {
110
+ let linkText = "";
111
+ let nextIdx = idx + 1;
112
+ while (nextIdx < tokens.length && tokens[nextIdx].type !== "link_close") {
113
+ if (tokens[nextIdx].type === "text" || tokens[nextIdx].type === "code_inline") linkText += tokens[nextIdx].content;
114
+ else if (tokens[nextIdx].children) {
115
+ for (const child of tokens[nextIdx].children) if (child.type === "text" || child.type === "code_inline") linkText += child.content;
116
+ }
117
+ nextIdx++;
118
+ }
119
+ const url = new URL(href);
120
+ url.searchParams.set("utm_source", hostname);
121
+ url.searchParams.set("utm_medium", "link");
122
+ url.searchParams.set("utm_content", "textlink");
123
+ if (linkText) url.searchParams.set("utm_term", linkText);
124
+ token.attrSet("href", url.toString());
125
+ }
126
+ return linkRule(tokens, idx, options, env, self);
127
+ };
128
+ });
129
+ }
130
+
131
+ //#endregion
132
+ //#region src/markdown-it/github-alerts.ts
133
+ function githubAlerts(md) {
134
+ md.use(MarkdownItGitHubAlerts);
135
+ }
136
+
137
+ //#endregion
138
+ //#region src/markdown-it/implicit-figures.ts
139
+ function implicitFiguresRule(md) {
140
+ md.use(implicitFigures, { figcaption: "alt" });
141
+ }
142
+
143
+ //#endregion
144
+ //#region src/markdown-it/link-attributes.ts
145
+ function linkAttributesRule(md) {
146
+ md.use(linkAttributes, [{
147
+ matcher: (link) => /^https?:\/\/(?:[a-z0-9-]+\.)?soubiran\.dev(?:[/?#]|$)/.test(link),
148
+ attrs: { target: "_blank" }
149
+ }, {
150
+ matcher: (link) => /^https?:\/\//.test(link),
151
+ attrs: {
152
+ target: "_blank",
153
+ rel: "noopener"
154
+ }
155
+ }]);
156
+ }
157
+
158
+ //#endregion
159
+ //#region src/markdown-it/shiki-highlight.ts
160
+ async function shikiHighlight(md) {
161
+ md.use(fromAsyncCodeToHtml(codeToHtml, {
162
+ defaultColor: false,
163
+ themes: {
164
+ light: "github-light",
165
+ dark: "github-dark"
166
+ }
167
+ }));
168
+ }
169
+
170
+ //#endregion
171
+ //#region src/markdown-it/table-of-contents.ts
172
+ function tableOfContentsRule(md) {
173
+ md.use((md$1) => {
174
+ md$1.renderer.rules.heading_open = (tokens, idx, options, _env, self) => {
175
+ const token = tokens[idx];
176
+ if (token.tag === "h2" || token.tag === "h3") {
177
+ const inlineToken = tokens[idx + 1];
178
+ const textContent = getTokensText(inlineToken.children);
179
+ token.attrSet("id", slugify(textContent));
180
+ return `<Heading :level="${token.tag.slice(1)}"${self.renderAttrs(token)}>`;
181
+ }
182
+ return self.renderToken(tokens, idx, options);
183
+ };
184
+ md$1.renderer.rules.heading_close = (tokens, idx, options, _env, self) => {
185
+ const token = tokens[idx];
186
+ if (token.tag === "h2" || token.tag === "h3") return "</Heading>";
187
+ return self.renderToken(tokens, idx, options);
188
+ };
189
+ });
190
+ md.use((md$1) => {
191
+ md$1.core.ruler.push("toc_to_env", (state) => {
192
+ const options = defaultOptions;
193
+ const tocTree = flatHeadlineItemsToNestedTree(findHeadlineElements(options.includeLevel, state.tokens, options));
194
+ state.env.frontmatter = state.env.frontmatter || {};
195
+ state.env.frontmatter.toc = tocTree.children || [];
196
+ return true;
197
+ });
198
+ });
199
+ }
200
+
201
+ //#endregion
202
+ //#region src/promise.ts
203
+ const promises = [];
204
+ async function resolveAll() {
205
+ await Promise.all(promises);
206
+ }
207
+
208
+ //#endregion
209
+ //#region src/og.ts
210
+ const ogSVG = fs.readFileSync(new URL("./og-template.svg", import.meta.url), "utf-8");
211
+ async function generate(title, hostname, output) {
212
+ if (fs.existsSync(output)) return;
213
+ await fs.mkdir(dirname(output), { recursive: true });
214
+ const lines = title.trim().split(/(.{0,30})(?:\s|$)/g).filter(Boolean);
215
+ const data = {
216
+ line1: lines[0],
217
+ line2: lines[1],
218
+ line3: lines[2],
219
+ headline: "",
220
+ hostname
221
+ };
222
+ const svg = ogSVG.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || "");
223
+ console.log(`Generating ${output}`);
224
+ try {
225
+ await sharp(Buffer.from(svg)).resize(1200 * 1.1, 630 * 1.1).png().toFile(output);
226
+ } catch (e) {
227
+ console.error("Failed to generate og image", e);
228
+ }
229
+ }
230
+ function og(id, frontmatter, hostname) {
231
+ (() => {
232
+ const path = `og/${basename(id, ".md")}.png`;
233
+ promises.push(generate(frontmatter.title.replace(/\s-\s.*$/, "").trim(), hostname, `public/${path}`));
234
+ frontmatter.image = `https://${hostname}/${path}`;
235
+ })();
236
+ }
237
+
238
+ //#endregion
239
+ //#region src/plugins/api.ts
240
+ /**
241
+ * Recursively scan a directory for markdown files
242
+ */
243
+ function scanMarkdownFiles(dir, baseDir) {
244
+ const files = [];
245
+ try {
246
+ const entries = readdirSync(dir, { withFileTypes: true });
247
+ for (const entry of entries) {
248
+ const fullPath = join(dir, entry.name);
249
+ if (entry.isDirectory()) files.push(...scanMarkdownFiles(fullPath, baseDir));
250
+ else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "index.md") files.push(fullPath);
251
+ }
252
+ } catch {}
253
+ return files;
254
+ }
255
+ /**
256
+ * Process a markdown file and extract its frontmatter
257
+ */
258
+ function processMarkdownFile(filePath, category) {
259
+ const { data } = matter(readFileSync(filePath, "utf-8"));
260
+ return {
261
+ path: `/${category}/${filePath.split("/").pop()?.replace(/\.md$/, "") || ""}`,
262
+ ...data
263
+ };
264
+ }
265
+ /**
266
+ * Generates the pages API JSON files in dist/api directory
267
+ */
268
+ async function api(config, categories) {
269
+ const pagesDir = resolve(cwd(), "pages");
270
+ const distDir = resolve(cwd(), config.build.outDir);
271
+ for (const name of categories) {
272
+ const processedFiles = scanMarkdownFiles(join(pagesDir, name)).map((file) => processMarkdownFile(file, name));
273
+ const apiDir = join(distDir, "api");
274
+ const path = join(apiDir, `${name}.json`);
275
+ mkdirSync(apiDir, { recursive: true });
276
+ writeFileSync(path, JSON.stringify(processedFiles, null, 2));
277
+ config.logger.info(`${dim(`${config.build.outDir}/`)}${cyan(path.replace(`${distDir}/`, ""))}`);
278
+ }
279
+ }
280
+ function apiPlugin(categories = []) {
281
+ let config;
282
+ return {
283
+ name: "api",
284
+ configResolved(resolvedConfig) {
285
+ config = resolvedConfig;
286
+ },
287
+ closeBundle() {
288
+ if (this.environment.name !== "client") return;
289
+ if (categories.length === 0) return;
290
+ const time = /* @__PURE__ */ new Date();
291
+ config.logger.info(yellow("Generate API files"));
292
+ api(config, categories);
293
+ config.logger.info(green(`✓ generated in ${(/* @__PURE__ */ new Date()).getTime() - time.getTime()}ms`));
294
+ }
295
+ };
296
+ }
297
+
298
+ //#endregion
299
+ //#region src/plugins/markdown.ts
300
+ /**
301
+ * Sanitize markdown content for LLM consumption
302
+ * - Removes HTML tags while preserving content inside paired tags
303
+ * - Adds title as H1 heading at the top
304
+ * - Safe for build-time processing
305
+ */
306
+ function sanitizeMarkdown(content, title) {
307
+ let sanitized = content;
308
+ let prevSanitized = "";
309
+ while (sanitized !== prevSanitized) {
310
+ prevSanitized = sanitized;
311
+ sanitized = sanitized.replace(/<[^>]+>([^<]*)<\/[^>]+>/g, "$1");
312
+ sanitized = sanitized.replace(/<[^>]*>/g, "");
313
+ }
314
+ if (title) sanitized = `# ${title}\n\n${sanitized}`;
315
+ return sanitized.trim();
316
+ }
317
+ /**
318
+ * Recursively copy and sanitize markdown files from source to target directory
319
+ * - Parses frontmatter and removes it
320
+ * - Sanitizes HTML tags
321
+ * - Preserves directory structure
322
+ * - Converts /<something>/index.md to /<something>.md (except for root /index.md)
323
+ */
324
+ function copyAndSanitizeMarkdownFiles(config, sourceDir, targetDir, isRoot = true) {
325
+ const outDir = join(resolve(cwd()), config.build.outDir);
326
+ const entries = readdirSync(sourceDir);
327
+ for (const entry of entries) {
328
+ const sourcePath = join(sourceDir, entry);
329
+ if (statSync(sourcePath).isDirectory()) {
330
+ const newTargetDir = join(targetDir, entry);
331
+ if (!existsSync(newTargetDir)) mkdirSync(newTargetDir, { recursive: true });
332
+ copyAndSanitizeMarkdownFiles(config, sourcePath, newTargetDir, false);
333
+ } else if (entry.endsWith(".md")) {
334
+ let targetPath;
335
+ if (entry === "index.md" && !isRoot) {
336
+ const parentDirName = basename(sourceDir);
337
+ targetPath = join(dirname(targetDir), `${parentDirName}.md`);
338
+ } else targetPath = join(targetDir, entry);
339
+ const targetDirPath = dirname(targetPath);
340
+ if (!existsSync(targetDirPath)) mkdirSync(targetDirPath, { recursive: true });
341
+ const { data, content } = matter(readFileSync(sourcePath, "utf-8"));
342
+ const sanitizedContent = sanitizeMarkdown(content, data.title);
343
+ writeFileSync(targetPath, sanitizedContent, "utf-8");
344
+ config.logger.info(`${dim(`${config.build.outDir}/`)}${cyan(targetPath.replace(`${outDir}/`, ""))}`);
345
+ }
346
+ }
347
+ }
348
+ function markdownPlugin() {
349
+ let config;
350
+ return {
351
+ name: "markdown",
352
+ configResolved(resolvedConfig) {
353
+ config = resolvedConfig;
354
+ },
355
+ closeBundle() {
356
+ if (this.environment.name !== "client") return;
357
+ const pagesDir = resolve(cwd(), "pages");
358
+ const distDir = resolve(cwd(), config.build.outDir);
359
+ const time = /* @__PURE__ */ new Date();
360
+ config.logger.info(yellow("Copy and Sanitize Markdown"));
361
+ copyAndSanitizeMarkdownFiles(config, pagesDir, distDir);
362
+ config.logger.info(green(`✓ copied in ${(/* @__PURE__ */ new Date()).getTime() - time.getTime()}ms`));
363
+ }
364
+ };
365
+ }
366
+
367
+ //#endregion
368
+ //#region src/plugins/meta.ts
369
+ /**
370
+ * Generate a hash of the content for change detection
371
+ */
372
+ function generateContentHash(content) {
373
+ return createHash("sha256").update(content).digest("hex");
374
+ }
375
+ /**
376
+ * Recursively scan all markdown files and extract metadata
377
+ */
378
+ function scanPagesForMeta(pagesDir, baseUri = "") {
379
+ const pages = [];
380
+ if (!existsSync(pagesDir)) return pages;
381
+ const entries = readdirSync(pagesDir);
382
+ for (const entry of entries) {
383
+ const fullPath = join(pagesDir, entry);
384
+ if (statSync(fullPath).isDirectory()) {
385
+ const subPages = scanPagesForMeta(fullPath, `${baseUri}/${entry}`);
386
+ pages.push(...subPages);
387
+ } else if (entry.endsWith(".md")) {
388
+ const parsed = matter(readFileSync(fullPath, "utf-8"));
389
+ let uri = baseUri;
390
+ if (entry !== "index.md") uri = joinURL(baseUri, entry.replace(/\.md$/, ""));
391
+ uri = withoutTrailingSlash(uri);
392
+ pages.push({
393
+ uri,
394
+ title: parsed.data.title,
395
+ description: parsed.data.description,
396
+ content: parsed.content,
397
+ id: parsed.data.id,
398
+ filePath: fullPath
399
+ });
400
+ }
401
+ }
402
+ return pages;
403
+ }
404
+ /**
405
+ * Generate meta.json file with all pages metadata
406
+ */
407
+ async function generateMeta(config, hostname) {
408
+ const pagesDir = resolve(cwd(), "pages");
409
+ const distDir = resolve(cwd(), config.build.outDir);
410
+ const pages = scanPagesForMeta(pagesDir).filter((page) => page.title).map((page) => ({
411
+ id: page.id,
412
+ title: page.title,
413
+ description: page.description,
414
+ uri: page.uri,
415
+ url: joinURL(`https://${hostname}`, page.uri),
416
+ hash: generateContentHash(page.content)
417
+ }));
418
+ pages.sort((a, b) => a.uri.localeCompare(b.uri));
419
+ const metaPath = join(distDir, "meta.json");
420
+ writeFileSync(metaPath, JSON.stringify(pages, null, 2));
421
+ config.logger.info(`${dim(`${config.build.outDir}/`)}${cyan(metaPath.replace(`${distDir}/`, ""))}`);
422
+ }
423
+ function metaPlugin(hostname) {
424
+ let config;
425
+ return {
426
+ name: "meta",
427
+ configResolved(resolvedConfig) {
428
+ config = resolvedConfig;
429
+ },
430
+ closeBundle() {
431
+ if (this.environment.name !== "client") return;
432
+ const time = /* @__PURE__ */ new Date();
433
+ config.logger.info(yellow("Generate meta.json"));
434
+ generateMeta(config, hostname);
435
+ config.logger.info(green(`✓ generated in ${(/* @__PURE__ */ new Date()).getTime() - time.getTime()}ms`));
436
+ }
437
+ };
438
+ }
439
+
440
+ //#endregion
441
+ //#region src/sitemap.ts
442
+ const routes = /* @__PURE__ */ new Set();
443
+ function sitemap(config, hostname, routes$1) {
444
+ const sitemapStream = new SitemapStream({ hostname: `https://${hostname}` });
445
+ const writeStream = createWriteStream(join(config.build.outDir, "sitemap.xml"));
446
+ sitemapStream.pipe(writeStream);
447
+ routes$1.forEach((item) => sitemapStream.write(item));
448
+ sitemapStream.end();
449
+ }
450
+
451
+ //#endregion
452
+ //#region src/structured-data/article.ts
453
+ /**
454
+ * @see https://developer.yoast.com/features/schema/pieces/article/
455
+ */
456
+ function article(id, structuredData$1, properties, options) {
457
+ const { title, description } = properties;
458
+ return { data: {
459
+ "@type": "Article",
460
+ "@id": joinURL(toUrl(options.hostname), "#", "schema", "Article", getUri(id)),
461
+ "headline": title,
462
+ "description": description,
463
+ "isPartOf": { "@id": structuredData$1.webpage.data["@id"] },
464
+ "mainEntityOfPage": { "@id": structuredData$1.webpage.data["@id"] },
465
+ "datePublished": structuredData$1.webpage.data.datePublished ? structuredData$1.webpage.data.datePublished : void 0,
466
+ "author": { "@id": structuredData$1.person.data["@id"] },
467
+ "publisher": { "@id": structuredData$1.person.data["@id"] },
468
+ "inLanguage": structuredData$1.webpage.data.inLanguage
469
+ } };
470
+ }
471
+
472
+ //#endregion
473
+ //#region src/structured-data/breadcrumb.ts
474
+ /**
475
+ * @see https://developer.yoast.com/features/schema/pieces/breadcrumb/
476
+ */
477
+ function breadcrumb(id, items, options) {
478
+ return { data: {
479
+ "@type": "BreadcrumbList",
480
+ "@id": joinURL(toUrl(options.hostname), "#", "schema", "BreadcrumbList", getUri(id)),
481
+ "itemListElement": items.map((item, index) => ({
482
+ "@type": "ListItem",
483
+ "position": index + 1,
484
+ "name": item.title,
485
+ ...item.type && item.url ? { item: {
486
+ "@type": item.type,
487
+ "@id": item.url
488
+ } } : {}
489
+ }))
490
+ } };
491
+ }
492
+
493
+ //#endregion
494
+ //#region src/structured-data/person.ts
495
+ /**
496
+ * @see https://developer.yoast.com/features/schema/pieces/person/
497
+ */
498
+ function person(options, personOptions) {
499
+ return { data: {
500
+ "@type": "Person",
501
+ "@id": joinURL(options.url, "#", "schema", "Person", "1"),
502
+ "name": personOptions.name,
503
+ "sameAs": personOptions.sameAs
504
+ } };
505
+ }
506
+
507
+ //#endregion
508
+ //#region src/structured-data/webpage.ts
509
+ /**
510
+ * @see https://developer.yoast.com/features/schema/pieces/webpage/
511
+ */
512
+ function webpage(id, structuredData$1, properties, options) {
513
+ const { title, description, datePublished, keywords } = properties;
514
+ const canonicalUrl = getCanonicalUrl(id, options.hostname);
515
+ const data = {
516
+ "@type": "WebPage",
517
+ "@id": canonicalUrl,
518
+ "url": canonicalUrl,
519
+ "name": title,
520
+ "description": description,
521
+ "isPartOf": { "@id": structuredData$1.website.data["@id"] },
522
+ "inLanguage": "en-US",
523
+ "potentialAction": [{
524
+ "@type": "ReadAction",
525
+ "target": [canonicalUrl]
526
+ }],
527
+ ...datePublished ? { datePublished: datePublished.toISOString() } : {},
528
+ ...keywords ? { keywords } : {}
529
+ };
530
+ return {
531
+ data,
532
+ setBreadcrumb(breadcrumbData) {
533
+ data.breadcrumb = { "@id": breadcrumbData.data["@id"] };
534
+ },
535
+ setCollection() {
536
+ data["@type"] = "CollectionPage";
537
+ delete data.potentialAction;
538
+ }
539
+ };
540
+ }
541
+
542
+ //#endregion
543
+ //#region src/structured-data/website.ts
544
+ /**
545
+ * @see https://developer.yoast.com/features/schema/pieces/website/
546
+ */
547
+ function website(structuredData$1, options) {
548
+ return { data: {
549
+ "@type": "WebSite",
550
+ "@id": joinURL(options.url, "#", "schema", "WebSite", "1"),
551
+ "url": options.url,
552
+ "name": options.name,
553
+ "inLanguage": ["en-US"],
554
+ "publisher": { "@id": structuredData$1.person.data["@id"] }
555
+ } };
556
+ }
557
+
558
+ //#endregion
559
+ //#region src/structured-data/index.ts
560
+ function structuredData(id, frontmatter, options) {
561
+ const { name, hostname, extractPage, getPageConfig } = options;
562
+ const graph = {
563
+ "@context": "https://schema.org",
564
+ "@graph": []
565
+ };
566
+ const structuredDataOptions = {
567
+ name,
568
+ hostname,
569
+ url: toUrl(hostname)
570
+ };
571
+ const personData = person(structuredDataOptions, options.person);
572
+ const websiteData = website({ person: personData }, structuredDataOptions);
573
+ const webpageData = webpage(id, { website: websiteData }, {
574
+ title: frontmatter.title,
575
+ description: frontmatter.description,
576
+ datePublished: frontmatter.date ? new Date(frontmatter.date) : void 0,
577
+ keywords: frontmatter.tags
578
+ }, structuredDataOptions);
579
+ const page = extractPage(id);
580
+ const pageConfig = getPageConfig?.(page, frontmatter);
581
+ if (pageConfig?.type === "article") {
582
+ const articleData = article(id, {
583
+ person: personData,
584
+ webpage: webpageData
585
+ }, {
586
+ title: frontmatter.title,
587
+ description: frontmatter.description
588
+ }, structuredDataOptions);
589
+ graph["@graph"].push(articleData.data);
590
+ if (pageConfig.breadcrumbItems) {
591
+ const breadcrumbData = breadcrumb(id, pageConfig.breadcrumbItems, structuredDataOptions);
592
+ graph["@graph"].push(breadcrumbData.data);
593
+ webpageData.setBreadcrumb(breadcrumbData);
594
+ }
595
+ } else if (pageConfig?.type === "collection") webpageData.setCollection();
596
+ graph["@graph"].push(personData.data, websiteData.data, webpageData.data);
597
+ frontmatter.script ??= [];
598
+ frontmatter.script.push({
599
+ type: "application/ld+json",
600
+ innerHTML: JSON.stringify(graph)
601
+ });
602
+ }
603
+
604
+ //#endregion
605
+ //#region vite.config.ts
606
+ var vite_config_default = (title, hostname, options, config = {}) => mergeConfig(defineConfig({
607
+ plugins: [
608
+ vueRouter({
609
+ extensions: [".vue", ".md"],
610
+ routesFolder: "pages",
611
+ dts: "src/typed-router.d.ts",
612
+ extendRoute(route) {
613
+ const path = route.components.get("default");
614
+ if (!path) return;
615
+ if (path.endsWith(".vue")) route.addToMeta({ frontmatter: { page: options.extractPage(path) } });
616
+ if (path.endsWith(".md")) {
617
+ const { data } = matter(readFileSync(path, "utf-8"));
618
+ route.addToMeta({ frontmatter: data });
619
+ }
620
+ }
621
+ }),
622
+ vue({ include: [/\.vue$/, /\.md$/] }),
623
+ ui({
624
+ autoImport: {
625
+ dts: "src/auto-imports.d.ts",
626
+ dirs: ["src/composables"],
627
+ imports: [
628
+ "vue",
629
+ "vue-router",
630
+ "@vueuse/core",
631
+ unheadVueComposablesImports,
632
+ {
633
+ from: "tailwind-variants",
634
+ imports: ["tv"]
635
+ },
636
+ soubiranComposablesImports
637
+ ]
638
+ },
639
+ components: {
640
+ include: [
641
+ /\.vue$/,
642
+ /\.vue\?vue/,
643
+ /\.md$/
644
+ ],
645
+ dts: "src/components.d.ts",
646
+ resolvers: [soubiranResolver()]
647
+ },
648
+ ui: { colors: { neutral: "neutral" } }
649
+ }),
650
+ markdown({
651
+ headEnabled: true,
652
+ wrapperClasses: [
653
+ "slide-enter-content",
654
+ "max-w-none",
655
+ "prose prose-neutral dark:prose-invert",
656
+ "prose-headings:text-default prose-h2:text-[1.125em] prose-h2:mb-[0.5em] prose-h3:text-[1em]",
657
+ "prose-p:my-[1em] dark:prose-p:text-muted",
658
+ "dark:prose-ul:text-muted dark:prose-ol:text-muted",
659
+ "dark:prose-strong:text-default",
660
+ "dark:prose-a:text-muted prose-a:font-semibold prose-a:no-underline prose-a:border-b prose-a:border-muted prose-a:transition-colors prose-a:duration-300 prose-a:ease-out prose-a:hover:border-[var(--ui-text-dimmed)]",
661
+ "prose-hr:max-w-1/2 prose-hr:mx-auto prose-hr:my-[2em]",
662
+ "prose-figure:bg-neutral-100 dark:prose-figure:bg-neutral-800 prose-figure:rounded-lg",
663
+ "prose-img:rounded-lg prose-img:border prose-img:border-accented prose-img:shadow-md",
664
+ "prose-video:rounded-lg prose-video:border prose-video:border-accented prose-video:shadow-md",
665
+ "prose-figcaption:text-center prose-figcaption:py-1 prose-figcaption:m-0",
666
+ "[&_:first-child]:mt-0 [&_:last-child]:mb-0"
667
+ ],
668
+ transforms: options.markdown?.transforms ?? {},
669
+ wrapperComponent: options.markdown?.wrapperComponent,
670
+ async markdownItSetup(md) {
671
+ githubAlerts(md);
672
+ implicitFiguresRule(md);
673
+ linkAttributesRule(md);
674
+ tableOfContentsRule(md);
675
+ customLink(md, hostname);
676
+ customImage(md, hostname);
677
+ await shikiHighlight(md);
678
+ },
679
+ frontmatterPreprocess(frontmatter, frontmatterOptions, id, defaults) {
680
+ createAssert(options.seo.assert?.rules)(id, frontmatter);
681
+ og(id, frontmatter, hostname);
682
+ canonical(id, frontmatter, hostname);
683
+ structuredData(id, frontmatter, {
684
+ name: title,
685
+ hostname,
686
+ person: options.seo.person,
687
+ extractPage: options.extractPage,
688
+ getPageConfig: options.seo.structuredData?.pageConfig
689
+ });
690
+ frontmatter.page = options.extractPage(id);
691
+ return {
692
+ head: defaults(frontmatter, frontmatterOptions),
693
+ frontmatter
694
+ };
695
+ }
696
+ }),
697
+ fonts({ google: { families: [
698
+ {
699
+ name: "DM Sans",
700
+ styles: "ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000"
701
+ },
702
+ {
703
+ name: "DM Mono",
704
+ styles: "ital,wght@0,300;0,400;0,500;1,300;1,400;1,500"
705
+ },
706
+ {
707
+ name: "Sofia Sans",
708
+ styles: "ital,wght@0,1..1000;1,1..1000"
709
+ }
710
+ ] } }),
711
+ icons({ autoInstall: true }),
712
+ apiPlugin(options.apiCategories),
713
+ markdownPlugin(),
714
+ metaPlugin(hostname),
715
+ {
716
+ name: "await",
717
+ async closeBundle() {
718
+ await resolveAll();
719
+ }
720
+ },
721
+ {
722
+ name: "extract-config",
723
+ configResolved(resolvedConfig) {
724
+ Object.assign(config, resolvedConfig);
725
+ }
726
+ }
727
+ ],
728
+ optimizeDeps: {
729
+ include: [
730
+ "vue",
731
+ "ofetch",
732
+ "reka-ui",
733
+ "vue-router",
734
+ "@unhead/vue",
735
+ "partysocket",
736
+ "@iconify/vue"
737
+ ],
738
+ exclude: ["@soubiran/ui"]
739
+ },
740
+ resolve: { alias: { "@": resolve("./src") } },
741
+ ssgOptions: {
742
+ formatting: "minify",
743
+ onPageRendered(route, renderedHTML) {
744
+ routes.add(route);
745
+ return renderedHTML;
746
+ },
747
+ onFinished() {
748
+ sitemap(config, hostname, Array.from(routes));
749
+ }
750
+ }
751
+ }), config);
752
+
753
+ //#endregion
754
+ export { vite_config_default as default };
package/package.json CHANGED
@@ -1,4 +1,65 @@
1
1
  {
2
2
  "name": "@soubiran/vite",
3
- "version": "0.0.0"
4
- }
3
+ "type": "module",
4
+ "version": "0.1.2",
5
+ "author": "Estéban Soubiran <esteban@soubiran.dev>",
6
+ "license": "MIT",
7
+ "funding": "https://github.com/sponsors/Barbapapazes",
8
+ "homepage": "https://github.com/Barbapapazes/.soubiran.dev",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/Barbapapazes/.soubiran.dev"
12
+ },
13
+ "bugs": "https://github.com/Barbapapazes/.soubiran.dev/issues",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/vite.config.d.ts",
17
+ "import": "./dist/vite.config.mjs"
18
+ }
19
+ },
20
+ "main": "dist/vite.config.mjs",
21
+ "types": "dist/vite.config.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "dependencies": {
26
+ "@nuxt/ui": "^4.2.0",
27
+ "@shikijs/markdown-it": "^3.15.0",
28
+ "@types/fs-extra": "^11.0.4",
29
+ "@types/markdown-it": "^14.1.2",
30
+ "@types/markdown-it-link-attributes": "^3.0.5",
31
+ "@types/node": "^24.10.1",
32
+ "@unhead/vue": "^2.0.19",
33
+ "@unpic/placeholder": "^0.1.2",
34
+ "@vitejs/plugin-vue": "^6.0.2",
35
+ "ansis": "^4.2.0",
36
+ "fs-extra": "^11.3.2",
37
+ "gray-matter": "^4.0.3",
38
+ "markdown-it": "^14.1.0",
39
+ "markdown-it-async": "^2.2.0",
40
+ "markdown-it-github-alerts": "^1.0.0",
41
+ "markdown-it-image-figures": "^2.1.1",
42
+ "markdown-it-link-attributes": "^4.0.1",
43
+ "markdown-it-table-of-contents": "^1.1.0",
44
+ "sharp": "^0.34.5",
45
+ "shiki": "^3.15.0",
46
+ "sitemap": "^8.0.2",
47
+ "typescript": "^5.9.3",
48
+ "ufo": "^1.6.1",
49
+ "unhead": "^2.0.19",
50
+ "unplugin-fonts": "^1.4.0",
51
+ "unplugin-icons": "^22.5.0",
52
+ "unplugin-vue-markdown": "^29.2.0",
53
+ "unplugin-vue-router": "^0.16.2",
54
+ "vite": "npm:rolldown-vite@7.1.20",
55
+ "vite-ssg": "^28.2.2",
56
+ "vue-router": "^4.6.3",
57
+ "@soubiran/ui": "0.1.2"
58
+ },
59
+ "devDependencies": {
60
+ "tsdown": "^0.18.3"
61
+ },
62
+ "scripts": {
63
+ "build": "tsdown"
64
+ }
65
+ }