@soubiran/vite 0.4.0 → 0.6.0

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.
@@ -1,18 +1,21 @@
1
- import { getUri, toUrl } from "./src/utils.mjs";
1
+ import { a as vueIncludePatterns, i as markdownExtensionRE, n as toUrl, r as componentIncludePatterns, t as getUri } from "./utils-DYsc-HAW.mjs";
2
2
  import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, join, resolve } from "node:path";
4
3
  import ui from "@nuxt/ui/vite";
5
4
  import soubiranComposablesImports from "@soubiran/ui/imports";
6
5
  import soubiranResolver from "@soubiran/ui/resolver";
6
+ import soubiranWrapperClasses from "@soubiran/ui/wrapper-classes";
7
7
  import { unheadVueComposablesImports } from "@unhead/vue";
8
8
  import vue from "@vitejs/plugin-vue";
9
9
  import matter from "gray-matter";
10
10
  import fonts from "unplugin-fonts/vite";
11
11
  import icons from "unplugin-icons/vite";
12
12
  import markdown from "unplugin-vue-markdown/vite";
13
- import vueRouter from "unplugin-vue-router/vite";
14
- import { defineConfig, mergeConfig } from "vite";
13
+ import router from "unplugin-vue-router/vite";
15
14
  import { joinURL, withoutTrailingSlash } from "ufo";
15
+ import { Buffer } from "node:buffer";
16
+ import { basename, dirname, join, resolve } from "node:path";
17
+ import fs from "fs-extra";
18
+ import sharp from "sharp";
16
19
  import { mkdir, readFile, writeFile } from "node:fs/promises";
17
20
  import { blurhashToDataUri } from "@unpic/placeholder";
18
21
  import MarkdownItGitHubAlerts from "markdown-it-github-alerts";
@@ -20,14 +23,11 @@ import implicitFigures from "markdown-it-image-figures";
20
23
  import linkAttributes from "markdown-it-link-attributes";
21
24
  import { fromAsyncCodeToHtml } from "@shikijs/markdown-it/async";
22
25
  import { codeToHtml } from "shiki";
23
- import { Buffer } from "node:buffer";
24
- import fs from "fs-extra";
25
- import sharp from "sharp";
26
- import { cwd } from "node:process";
27
26
  import { cyan, dim, green, yellow } from "ansis";
27
+ import { cwd } from "node:process";
28
28
  import { createHash } from "node:crypto";
29
29
  import { SitemapStream } from "sitemap";
30
- //#region src/assert.ts
30
+ //#region src/markdown/assert.ts
31
31
  function createAssert(customAssert) {
32
32
  return (id, frontmatter) => {
33
33
  if (frontmatter.description) {
@@ -38,7 +38,7 @@ function createAssert(customAssert) {
38
38
  };
39
39
  }
40
40
  //#endregion
41
- //#region src/canonical.ts
41
+ //#region src/markdown/canonical.ts
42
42
  function getCanonicalUrl(id, hostname) {
43
43
  return joinURL(toUrl(hostname), getUri(id));
44
44
  }
@@ -56,6 +56,226 @@ function canonical(id, frontmatter, hostname) {
56
56
  });
57
57
  }
58
58
  //#endregion
59
+ //#region src/domain/promise.ts
60
+ const promises = [];
61
+ async function resolveAll() {
62
+ await Promise.all(promises);
63
+ }
64
+ //#endregion
65
+ //#region src/markdown/og.ts
66
+ const ogSVG = fs.readFileSync(new URL("./og-template.svg", import.meta.url), "utf-8");
67
+ const titleBreakRE = /(.{0,30})(?:\s|$)/g;
68
+ const templateTokenRE = /\{\{([^}]+)\}\}/g;
69
+ const titleSuffixRE = /\s-\s.*$/;
70
+ async function generate(title, hostname, output) {
71
+ if (fs.existsSync(output)) return;
72
+ await fs.mkdir(dirname(output), { recursive: true });
73
+ const lines = title.trim().split(titleBreakRE).filter(Boolean);
74
+ const data = {
75
+ line1: lines[0],
76
+ line2: lines[1],
77
+ line3: lines[2],
78
+ headline: "",
79
+ hostname
80
+ };
81
+ const svg = ogSVG.replace(templateTokenRE, (_, name) => data[name] || "");
82
+ console.log(`Generating ${output}`);
83
+ try {
84
+ await sharp(Buffer.from(svg)).resize(1200 * 1.1, 630 * 1.1).png().toFile(output);
85
+ } catch (e) {
86
+ console.error("Failed to generate og image", e);
87
+ }
88
+ }
89
+ function og(id, frontmatter, hostname) {
90
+ (() => {
91
+ const path = `og/${basename(id, ".md")}.png`;
92
+ promises.push(generate(frontmatter.title.replace(titleSuffixRE, "").trim(), hostname, `public/${path}`));
93
+ frontmatter.image = `https://${hostname}/${path}`;
94
+ })();
95
+ }
96
+ //#endregion
97
+ //#region src/markdown/structured-data/constants.ts
98
+ const DEFAULT_PERSON = {
99
+ name: "Estéban Soubiran",
100
+ sameAs: [
101
+ "https://x.com/soubiran_",
102
+ "https://www.linkedin.com/in/esteban25",
103
+ "https://www.twitch.tv/barbapapazes",
104
+ "https://www.youtube.com/@barbapapazes",
105
+ "https://github.com/barbapapazes",
106
+ "https://soubiran.dev",
107
+ "https://esteban-soubiran.site",
108
+ "https://barbapapazes.dev"
109
+ ]
110
+ };
111
+ //#endregion
112
+ //#region src/markdown/structured-data/schemas/article.ts
113
+ /**
114
+ * @see https://developer.yoast.com/features/schema/pieces/article/
115
+ */
116
+ function article(id, structuredData, properties, options) {
117
+ const { title, description } = properties;
118
+ return { data: {
119
+ "@type": "Article",
120
+ "@id": joinURL(toUrl(options.hostname), "#", "schema", "Article", getUri(id)),
121
+ "headline": title,
122
+ "description": description,
123
+ "isPartOf": { "@id": structuredData.webpage.data["@id"] },
124
+ "mainEntityOfPage": { "@id": structuredData.webpage.data["@id"] },
125
+ "datePublished": structuredData.webpage.data.datePublished ? structuredData.webpage.data.datePublished : void 0,
126
+ "author": { "@id": structuredData.person.data["@id"] },
127
+ "publisher": { "@id": structuredData.person.data["@id"] },
128
+ "inLanguage": structuredData.webpage.data.inLanguage
129
+ } };
130
+ }
131
+ //#endregion
132
+ //#region src/markdown/structured-data/schemas/breadcrumb.ts
133
+ /**
134
+ * @see https://developer.yoast.com/features/schema/pieces/breadcrumb/
135
+ */
136
+ function breadcrumb(id, items, options) {
137
+ return { data: {
138
+ "@type": "BreadcrumbList",
139
+ "@id": joinURL(toUrl(options.hostname), "#", "schema", "BreadcrumbList", getUri(id)),
140
+ "itemListElement": items.map((item, index) => ({
141
+ "@type": "ListItem",
142
+ "position": index + 1,
143
+ "name": item.title,
144
+ ...item.type && item.url ? { item: {
145
+ "@type": item.type,
146
+ "@id": item.url
147
+ } } : {}
148
+ }))
149
+ } };
150
+ }
151
+ //#endregion
152
+ //#region src/markdown/structured-data/schemas/person.ts
153
+ /**
154
+ * @see https://developer.yoast.com/features/schema/pieces/person/
155
+ */
156
+ function person(properties, options) {
157
+ return { data: {
158
+ "@type": "Person",
159
+ "@id": joinURL(options.url, "#", "schema", "Person", "1"),
160
+ "name": properties.name,
161
+ "sameAs": properties.sameAs
162
+ } };
163
+ }
164
+ //#endregion
165
+ //#region src/markdown/structured-data/schemas/webpage.ts
166
+ /**
167
+ * @see https://developer.yoast.com/features/schema/pieces/webpage/
168
+ */
169
+ function webpage(id, structuredData, properties, options) {
170
+ const { title, description, datePublished, keywords } = properties;
171
+ const canonicalUrl = getCanonicalUrl(id, options.hostname);
172
+ const data = {
173
+ "@type": "WebPage",
174
+ "@id": canonicalUrl,
175
+ "url": canonicalUrl,
176
+ "name": title,
177
+ "description": description,
178
+ "isPartOf": { "@id": structuredData.website.data["@id"] },
179
+ "inLanguage": "en-US",
180
+ "potentialAction": [{
181
+ "@type": "ReadAction",
182
+ "target": [canonicalUrl]
183
+ }],
184
+ ...datePublished ? { datePublished: datePublished.toISOString() } : {},
185
+ ...keywords ? { keywords } : {}
186
+ };
187
+ return {
188
+ data,
189
+ setBreadcrumb(breadcrumbData) {
190
+ data.breadcrumb = { "@id": breadcrumbData.data["@id"] };
191
+ },
192
+ setCollection() {
193
+ data["@type"] = "CollectionPage";
194
+ delete data.potentialAction;
195
+ }
196
+ };
197
+ }
198
+ //#endregion
199
+ //#region src/markdown/structured-data/schemas/website.ts
200
+ /**
201
+ * @see https://developer.yoast.com/features/schema/pieces/website/
202
+ */
203
+ function website(structuredData, options) {
204
+ return { data: {
205
+ "@type": "WebSite",
206
+ "@id": joinURL(options.url, "#", "schema", "WebSite", "1"),
207
+ "url": options.url,
208
+ "name": options.name,
209
+ "inLanguage": ["en-US"],
210
+ "publisher": { "@id": structuredData.person.data["@id"] }
211
+ } };
212
+ }
213
+ //#endregion
214
+ //#region src/markdown/structured-data/index.ts
215
+ function structuredData(id, frontmatter, options) {
216
+ const { name, hostname, extractPage, getPageConfig } = options;
217
+ const graph = {
218
+ "@context": "https://schema.org",
219
+ "@graph": []
220
+ };
221
+ const structuredDataOptions = {
222
+ name,
223
+ hostname,
224
+ url: toUrl(hostname)
225
+ };
226
+ const personData = person(DEFAULT_PERSON, structuredDataOptions);
227
+ const websiteData = website({ person: personData }, structuredDataOptions);
228
+ const webpageData = webpage(id, { website: websiteData }, {
229
+ title: frontmatter.title,
230
+ description: frontmatter.description,
231
+ datePublished: frontmatter.date ? new Date(frontmatter.date) : void 0,
232
+ keywords: frontmatter.tags
233
+ }, structuredDataOptions);
234
+ const page = extractPage(id);
235
+ const pageConfig = getPageConfig?.(page, frontmatter);
236
+ if (pageConfig?.type === "article") {
237
+ const articleData = article(id, {
238
+ person: personData,
239
+ webpage: webpageData
240
+ }, {
241
+ title: frontmatter.title,
242
+ description: frontmatter.description
243
+ }, structuredDataOptions);
244
+ graph["@graph"].push(articleData.data);
245
+ if (pageConfig.breadcrumbItems) {
246
+ const breadcrumbData = breadcrumb(id, pageConfig.breadcrumbItems, structuredDataOptions);
247
+ graph["@graph"].push(breadcrumbData.data);
248
+ webpageData.setBreadcrumb(breadcrumbData);
249
+ }
250
+ } else if (pageConfig?.type === "collection") webpageData.setCollection();
251
+ graph["@graph"].push(personData.data, websiteData.data, webpageData.data);
252
+ frontmatter.script ??= [];
253
+ frontmatter.script.push({
254
+ type: "application/ld+json",
255
+ innerHTML: JSON.stringify(graph)
256
+ });
257
+ }
258
+ //#endregion
259
+ //#region src/markdown/frontmatter.ts
260
+ function markdownFrontmatterFactory(options) {
261
+ return (frontmatter, frontmatterOptions, id, defaults) => {
262
+ createAssert(options.assertRules)(id, frontmatter);
263
+ og(id, frontmatter, options.hostname);
264
+ canonical(id, frontmatter, options.hostname);
265
+ structuredData(id, frontmatter, {
266
+ name: options.title,
267
+ hostname: options.hostname,
268
+ extractPage: options.extractPage,
269
+ getPageConfig: options.getPageConfig
270
+ });
271
+ frontmatter.page = options.extractPage(id);
272
+ return {
273
+ head: defaults(frontmatter, frontmatterOptions),
274
+ frontmatter
275
+ };
276
+ };
277
+ }
278
+ //#endregion
59
279
  //#region src/markdown-it/custom-image.ts
60
280
  function customImage(md, hostname) {
61
281
  md.use((md) => {
@@ -87,13 +307,14 @@ function customImage(md, hostname) {
87
307
  }
88
308
  //#endregion
89
309
  //#region src/markdown-it/custom-link.ts
310
+ const internalLinkRE$1 = /^https?:\/\/(?:[a-z0-9-]+\.)?soubiran\.dev(?:[/?#]|$)/;
90
311
  function customLink(md, hostname) {
91
312
  md.use((md) => {
92
313
  const linkRule = md.renderer.rules.link_open;
93
314
  md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
94
315
  const token = tokens[idx];
95
316
  const href = token.attrGet("href");
96
- if (href && /^https?:\/\/(?:[a-z0-9-]+\.)?soubiran\.dev(?:[/?#]|$)/.test(href)) {
317
+ if (href && internalLinkRE$1.test(href)) {
97
318
  let linkText = "";
98
319
  let nextIdx = idx + 1;
99
320
  while (nextIdx < tokens.length && tokens[nextIdx].type !== "link_close") {
@@ -126,12 +347,14 @@ function implicitFiguresRule(md) {
126
347
  }
127
348
  //#endregion
128
349
  //#region src/markdown-it/link-attributes.ts
350
+ const internalLinkRE = /^https?:\/\/(?:[a-z0-9-]+\.)?soubiran\.dev(?:[/?#]|$)/;
351
+ const externalLinkRE = /^https?:\/\//;
129
352
  function linkAttributesRule(md) {
130
353
  md.use(linkAttributes, [{
131
- matcher: (link) => /^https?:\/\/(?:[a-z0-9-]+\.)?soubiran\.dev(?:[/?#]|$)/.test(link),
354
+ matcher: (link) => internalLinkRE.test(link),
132
355
  attrs: { target: "_blank" }
133
356
  }, {
134
- matcher: (link) => /^https?:\/\//.test(link),
357
+ matcher: (link) => externalLinkRE.test(link),
135
358
  attrs: {
136
359
  target: "_blank",
137
360
  rel: "noopener"
@@ -371,52 +594,30 @@ function tableOfContentsRule(md) {
371
594
  });
372
595
  }
373
596
  //#endregion
374
- //#region src/promise.ts
375
- const promises = [];
376
- async function resolveAll() {
377
- await Promise.all(promises);
378
- }
379
- //#endregion
380
- //#region src/og.ts
381
- const ogSVG = fs.readFileSync(new URL("./og-template.svg", import.meta.url), "utf-8");
382
- async function generate(title, hostname, output) {
383
- if (fs.existsSync(output)) return;
384
- await fs.mkdir(dirname(output), { recursive: true });
385
- const lines = title.trim().split(/(.{0,30})(?:\s|$)/g).filter(Boolean);
386
- const data = {
387
- line1: lines[0],
388
- line2: lines[1],
389
- line3: lines[2],
390
- headline: "",
391
- hostname
597
+ //#region src/markdown/rules.ts
598
+ function markdownRulesFactory(hostname) {
599
+ return async (md) => {
600
+ githubAlerts(md);
601
+ implicitFiguresRule(md);
602
+ linkAttributesRule(md);
603
+ tableOfContentsRule(md);
604
+ customLink(md, hostname);
605
+ customImage(md, hostname);
606
+ await shikiHighlight(md);
392
607
  };
393
- const svg = ogSVG.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || "");
394
- console.log(`Generating ${output}`);
395
- try {
396
- await sharp(Buffer.from(svg)).resize(1200 * 1.1, 630 * 1.1).png().toFile(output);
397
- } catch (e) {
398
- console.error("Failed to generate og image", e);
399
- }
400
- }
401
- function og(id, frontmatter, hostname) {
402
- (() => {
403
- const path = `og/${basename(id, ".md")}.png`;
404
- promises.push(generate(frontmatter.title.replace(/\s-\s.*$/, "").trim(), hostname, `public/${path}`));
405
- frontmatter.image = `https://${hostname}/${path}`;
406
- })();
407
608
  }
408
609
  //#endregion
409
- //#region src/plugins/api.ts
610
+ //#region src/domain/api.ts
410
611
  /**
411
612
  * Recursively scan a directory for markdown files
412
613
  */
413
- function scanMarkdownFiles(dir, baseDir) {
614
+ function scanMarkdownFiles(dir) {
414
615
  const files = [];
415
616
  try {
416
617
  const entries = readdirSync(dir, { withFileTypes: true });
417
618
  for (const entry of entries) {
418
619
  const fullPath = join(dir, entry.name);
419
- if (entry.isDirectory()) files.push(...scanMarkdownFiles(fullPath, baseDir));
620
+ if (entry.isDirectory()) files.push(...scanMarkdownFiles(fullPath));
420
621
  else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "index.md") files.push(fullPath);
421
622
  }
422
623
  } catch {}
@@ -428,26 +629,28 @@ function scanMarkdownFiles(dir, baseDir) {
428
629
  function processMarkdownFile(filePath, category) {
429
630
  const { data } = matter(readFileSync(filePath, "utf-8"));
430
631
  return {
431
- path: `/${category}/${filePath.split("/").pop()?.replace(/\.md$/, "") || ""}`,
632
+ path: `/${category}/${filePath.split("/").pop()?.replace(markdownExtensionRE, "") || ""}`,
432
633
  ...data
433
634
  };
434
635
  }
435
636
  /**
436
637
  * Generates the pages API JSON files in dist/api directory
437
638
  */
438
- async function api(config, categories) {
639
+ function generateJsonApi(outDir, categories, logger) {
439
640
  const pagesDir = resolve(cwd(), "pages");
440
- const distDir = resolve(cwd(), config.build.outDir);
641
+ const distDir = resolve(cwd(), outDir);
441
642
  for (const name of categories) {
442
643
  const processedFiles = scanMarkdownFiles(join(pagesDir, name)).map((file) => processMarkdownFile(file, name));
443
644
  const apiDir = join(distDir, "api");
444
645
  const path = join(apiDir, `${name}.json`);
445
646
  mkdirSync(apiDir, { recursive: true });
446
647
  writeFileSync(path, JSON.stringify(processedFiles, null, 2));
447
- config.logger.info(`${dim(`${config.build.outDir}/`)}${cyan(path.replace(`${distDir}/`, ""))}`);
648
+ logger.info(`${dim(`${outDir}/`)}${cyan(path.replace(`${distDir}/`, ""))}`);
448
649
  }
449
650
  }
450
- function apiPlugin(categories = []) {
651
+ //#endregion
652
+ //#region src/plugins/api.ts
653
+ function api_default(categories = []) {
451
654
  let config;
452
655
  return {
453
656
  name: "api",
@@ -457,83 +660,28 @@ function apiPlugin(categories = []) {
457
660
  closeBundle() {
458
661
  if (this.environment.name !== "client") return;
459
662
  if (categories.length === 0) return;
460
- const time = /* @__PURE__ */ new Date();
663
+ const time = Date.now();
461
664
  config.logger.info(yellow("Generate API files"));
462
- api(config, categories);
463
- config.logger.info(green(`✓ generated in ${(/* @__PURE__ */ new Date()).getTime() - time.getTime()}ms`));
665
+ generateJsonApi(config.build.outDir, categories, config.logger);
666
+ config.logger.info(green(`✓ generated in ${Date.now() - time}ms`));
464
667
  }
465
668
  };
466
669
  }
467
670
  //#endregion
468
- //#region src/plugins/markdown.ts
469
- /**
470
- * Sanitize markdown content for LLM consumption
471
- * - Removes HTML tags while preserving content inside paired tags
472
- * - Adds title as H1 heading at the top
473
- * - Safe for build-time processing
474
- */
475
- function sanitizeMarkdown(content, title) {
476
- let sanitized = content;
477
- let prevSanitized = "";
478
- while (sanitized !== prevSanitized) {
479
- prevSanitized = sanitized;
480
- sanitized = sanitized.replace(/<[^>]+>([^<]*)<\/[^>]+>/g, "$1");
481
- sanitized = sanitized.replace(/<[^>]*>/g, "");
482
- }
483
- if (title) sanitized = `# ${title}\n\n${sanitized}`;
484
- return sanitized.trim();
485
- }
486
- /**
487
- * Recursively copy and sanitize markdown files from source to target directory
488
- * - Parses frontmatter and removes it
489
- * - Sanitizes HTML tags
490
- * - Preserves directory structure
491
- * - Converts /<something>/index.md to /<something>.md (except for root /index.md)
492
- */
493
- function copyAndSanitizeMarkdownFiles(config, sourceDir, targetDir, isRoot = true) {
494
- const outDir = join(resolve(cwd()), config.build.outDir);
495
- const entries = readdirSync(sourceDir);
496
- for (const entry of entries) {
497
- const sourcePath = join(sourceDir, entry);
498
- if (statSync(sourcePath).isDirectory()) {
499
- const newTargetDir = join(targetDir, entry);
500
- if (!existsSync(newTargetDir)) mkdirSync(newTargetDir, { recursive: true });
501
- copyAndSanitizeMarkdownFiles(config, sourcePath, newTargetDir, false);
502
- } else if (entry.endsWith(".md")) {
503
- let targetPath;
504
- if (entry === "index.md" && !isRoot) {
505
- const parentDirName = basename(sourceDir);
506
- targetPath = join(dirname(targetDir), `${parentDirName}.md`);
507
- } else targetPath = join(targetDir, entry);
508
- const targetDirPath = dirname(targetPath);
509
- if (!existsSync(targetDirPath)) mkdirSync(targetDirPath, { recursive: true });
510
- const { data, content } = matter(readFileSync(sourcePath, "utf-8"));
511
- const sanitizedContent = sanitizeMarkdown(content, data.title);
512
- writeFileSync(targetPath, sanitizedContent, "utf-8");
513
- config.logger.info(`${dim(`${config.build.outDir}/`)}${cyan(targetPath.replace(`${outDir}/`, ""))}`);
514
- }
515
- }
516
- }
517
- function markdownPlugin() {
518
- let config;
671
+ //#region src/plugins/config.ts
672
+ function config_default() {
519
673
  return {
520
- name: "markdown",
521
- configResolved(resolvedConfig) {
522
- config = resolvedConfig;
523
- },
524
- closeBundle() {
525
- if (this.environment.name !== "client") return;
526
- const pagesDir = resolve(cwd(), "pages");
527
- const distDir = resolve(cwd(), config.build.outDir);
528
- const time = /* @__PURE__ */ new Date();
529
- config.logger.info(yellow("Copy and Sanitize Markdown"));
530
- copyAndSanitizeMarkdownFiles(config, pagesDir, distDir);
531
- config.logger.info(green(`✓ copied in ${(/* @__PURE__ */ new Date()).getTime() - time.getTime()}ms`));
674
+ name: "soubiran:config",
675
+ config() {
676
+ return {
677
+ optimizeDeps: { exclude: ["@soubiran/ui"] },
678
+ resolve: { alias: { "@": resolve("./src") } }
679
+ };
532
680
  }
533
681
  };
534
682
  }
535
683
  //#endregion
536
- //#region src/plugins/meta.ts
684
+ //#region src/domain/meta.ts
537
685
  /**
538
686
  * Generate a hash of the content for change detection
539
687
  */
@@ -555,15 +703,14 @@ function scanPagesForMeta(pagesDir, baseUri = "") {
555
703
  } else if (entry.endsWith(".md")) {
556
704
  const parsed = matter(readFileSync(fullPath, "utf-8"));
557
705
  let uri = baseUri;
558
- if (entry !== "index.md") uri = joinURL(baseUri, entry.replace(/\.md$/, ""));
706
+ if (entry !== "index.md") uri = joinURL(baseUri, entry.replace(markdownExtensionRE, ""));
559
707
  uri = withoutTrailingSlash(uri);
560
708
  pages.push({
561
709
  uri,
562
710
  title: parsed.data.title,
563
711
  description: parsed.data.description,
564
712
  content: parsed.content,
565
- id: parsed.data.id,
566
- filePath: fullPath
713
+ id: parsed.data.id
567
714
  });
568
715
  }
569
716
  }
@@ -572,9 +719,9 @@ function scanPagesForMeta(pagesDir, baseUri = "") {
572
719
  /**
573
720
  * Generate meta.json file with all pages metadata
574
721
  */
575
- async function generateMeta(config, hostname) {
722
+ function generateMeta(outDir, hostname, logger) {
576
723
  const pagesDir = resolve(cwd(), "pages");
577
- const distDir = resolve(cwd(), config.build.outDir);
724
+ const distDir = resolve(cwd(), outDir);
578
725
  const pages = scanPagesForMeta(pagesDir).filter((page) => page.title).map((page) => ({
579
726
  id: page.id,
580
727
  title: page.title,
@@ -586,9 +733,11 @@ async function generateMeta(config, hostname) {
586
733
  pages.sort((a, b) => a.uri.localeCompare(b.uri));
587
734
  const metaPath = join(distDir, "meta.json");
588
735
  writeFileSync(metaPath, JSON.stringify(pages, null, 2));
589
- config.logger.info(`${dim(`${config.build.outDir}/`)}${cyan(metaPath.replace(`${distDir}/`, ""))}`);
736
+ logger.info(`${dim(`${outDir}/`)}${cyan(metaPath.replace(`${distDir}/`, ""))}`);
590
737
  }
591
- function metaPlugin(hostname) {
738
+ //#endregion
739
+ //#region src/plugins/meta.ts
740
+ function meta_default(hostname) {
592
741
  let config;
593
742
  return {
594
743
  name: "meta",
@@ -597,321 +746,216 @@ function metaPlugin(hostname) {
597
746
  },
598
747
  closeBundle() {
599
748
  if (this.environment.name !== "client") return;
600
- const time = /* @__PURE__ */ new Date();
749
+ const time = Date.now();
601
750
  config.logger.info(yellow("Generate meta.json"));
602
- generateMeta(config, hostname);
603
- config.logger.info(green(`✓ generated in ${(/* @__PURE__ */ new Date()).getTime() - time.getTime()}ms`));
751
+ generateMeta(config.build.outDir, hostname, config.logger);
752
+ config.logger.info(green(`✓ generated in ${Date.now() - time}ms`));
604
753
  }
605
754
  };
606
755
  }
607
756
  //#endregion
608
- //#region src/sitemap.ts
609
- const routes = /* @__PURE__ */ new Set();
610
- function sitemap(config, hostname, routes) {
611
- const sitemapStream = new SitemapStream({ hostname: `https://${hostname}` });
612
- const writeStream = createWriteStream(join(config.build.outDir, "sitemap.xml"));
613
- sitemapStream.pipe(writeStream);
614
- routes.forEach((item) => sitemapStream.write(item));
615
- sitemapStream.end();
616
- }
617
- //#endregion
618
- //#region src/structured-data/article.ts
619
- /**
620
- * @see https://developer.yoast.com/features/schema/pieces/article/
621
- */
622
- function article(id, structuredData, properties, options) {
623
- const { title, description } = properties;
624
- return { data: {
625
- "@type": "Article",
626
- "@id": joinURL(toUrl(options.hostname), "#", "schema", "Article", getUri(id)),
627
- "headline": title,
628
- "description": description,
629
- "isPartOf": { "@id": structuredData.webpage.data["@id"] },
630
- "mainEntityOfPage": { "@id": structuredData.webpage.data["@id"] },
631
- "datePublished": structuredData.webpage.data.datePublished ? structuredData.webpage.data.datePublished : void 0,
632
- "author": { "@id": structuredData.person.data["@id"] },
633
- "publisher": { "@id": structuredData.person.data["@id"] },
634
- "inLanguage": structuredData.webpage.data.inLanguage
635
- } };
757
+ //#region src/plugins/promise.ts
758
+ function promise_default() {
759
+ return {
760
+ name: "soubiran:promise",
761
+ async closeBundle() {
762
+ await resolveAll();
763
+ }
764
+ };
636
765
  }
637
766
  //#endregion
638
- //#region src/structured-data/breadcrumb.ts
767
+ //#region src/domain/raw-markdown.ts
768
+ const pairedHtmlTagRE = /<[^>]+>([^<]*)<\/[^>]+>/g;
769
+ const htmlTagRE = /<[^>]*>/g;
639
770
  /**
640
- * @see https://developer.yoast.com/features/schema/pieces/breadcrumb/
771
+ * Sanitize markdown content for LLM consumption
772
+ * - Removes HTML tags while preserving content inside paired tags
773
+ * - Adds title as H1 heading at the top
774
+ * - Safe for build-time processing
641
775
  */
642
- function breadcrumb(id, items, options) {
643
- return { data: {
644
- "@type": "BreadcrumbList",
645
- "@id": joinURL(toUrl(options.hostname), "#", "schema", "BreadcrumbList", getUri(id)),
646
- "itemListElement": items.map((item, index) => ({
647
- "@type": "ListItem",
648
- "position": index + 1,
649
- "name": item.title,
650
- ...item.type && item.url ? { item: {
651
- "@type": item.type,
652
- "@id": item.url
653
- } } : {}
654
- }))
655
- } };
776
+ function sanitizeMarkdown(content, title) {
777
+ let sanitized = content;
778
+ let prevSanitized = "";
779
+ while (sanitized !== prevSanitized) {
780
+ prevSanitized = sanitized;
781
+ sanitized = sanitized.replace(pairedHtmlTagRE, "$1");
782
+ sanitized = sanitized.replace(htmlTagRE, "");
783
+ }
784
+ if (title) sanitized = `# ${title}\n\n${sanitized}`;
785
+ return sanitized.trim();
656
786
  }
657
- //#endregion
658
- //#region src/structured-data/person.ts
659
787
  /**
660
- * @see https://developer.yoast.com/features/schema/pieces/person/
788
+ * Recursively copy and sanitize markdown files from source to target directory
789
+ * - Parses frontmatter and removes it
790
+ * - Sanitizes HTML tags
791
+ * - Preserves directory structure
792
+ * - Converts /<something>/index.md to /<something>.md (except for root /index.md)
661
793
  */
662
- function person(options, personOptions) {
663
- return { data: {
664
- "@type": "Person",
665
- "@id": joinURL(options.url, "#", "schema", "Person", "1"),
666
- "name": personOptions.name,
667
- "sameAs": personOptions.sameAs
668
- } };
794
+ function copyAndSanitizeMarkdownFiles(outDir, logger, sourceDir, targetDir, isRoot = true) {
795
+ const outDirPath = join(resolve(cwd()), outDir);
796
+ const entries = readdirSync(sourceDir);
797
+ for (const entry of entries) {
798
+ const sourcePath = join(sourceDir, entry);
799
+ if (statSync(sourcePath).isDirectory()) {
800
+ const newTargetDir = join(targetDir, entry);
801
+ if (!existsSync(newTargetDir)) mkdirSync(newTargetDir, { recursive: true });
802
+ copyAndSanitizeMarkdownFiles(outDir, logger, sourcePath, newTargetDir, false);
803
+ } else if (entry.endsWith(".md")) {
804
+ let targetPath;
805
+ if (entry === "index.md" && !isRoot) {
806
+ const parentDirName = basename(sourceDir);
807
+ targetPath = join(dirname(targetDir), `${parentDirName}.md`);
808
+ } else targetPath = join(targetDir, entry);
809
+ const targetDirPath = dirname(targetPath);
810
+ if (!existsSync(targetDirPath)) mkdirSync(targetDirPath, { recursive: true });
811
+ const { data, content } = matter(readFileSync(sourcePath, "utf-8"));
812
+ const sanitizedContent = sanitizeMarkdown(content, data.title);
813
+ writeFileSync(targetPath, sanitizedContent, "utf-8");
814
+ logger.info(`${dim(`${outDir}/`)}${cyan(targetPath.replace(`${outDirPath}/`, ""))}`);
815
+ }
816
+ }
669
817
  }
670
818
  //#endregion
671
- //#region src/structured-data/webpage.ts
672
- /**
673
- * @see https://developer.yoast.com/features/schema/pieces/webpage/
674
- */
675
- function webpage(id, structuredData, properties, options) {
676
- const { title, description, datePublished, keywords } = properties;
677
- const canonicalUrl = getCanonicalUrl(id, options.hostname);
678
- const data = {
679
- "@type": "WebPage",
680
- "@id": canonicalUrl,
681
- "url": canonicalUrl,
682
- "name": title,
683
- "description": description,
684
- "isPartOf": { "@id": structuredData.website.data["@id"] },
685
- "inLanguage": "en-US",
686
- "potentialAction": [{
687
- "@type": "ReadAction",
688
- "target": [canonicalUrl]
689
- }],
690
- ...datePublished ? { datePublished: datePublished.toISOString() } : {},
691
- ...keywords ? { keywords } : {}
692
- };
819
+ //#region src/plugins/raw-markdown.ts
820
+ function raw_markdown_default() {
821
+ let config;
693
822
  return {
694
- data,
695
- setBreadcrumb(breadcrumbData) {
696
- data.breadcrumb = { "@id": breadcrumbData.data["@id"] };
823
+ name: "markdown",
824
+ configResolved(resolvedConfig) {
825
+ config = resolvedConfig;
697
826
  },
698
- setCollection() {
699
- data["@type"] = "CollectionPage";
700
- delete data.potentialAction;
827
+ closeBundle() {
828
+ if (this.environment.name !== "client") return;
829
+ const pagesDir = resolve(cwd(), "pages");
830
+ const distDir = resolve(cwd(), config.build.outDir);
831
+ const time = Date.now();
832
+ config.logger.info(yellow("Copy and Sanitize Markdown"));
833
+ copyAndSanitizeMarkdownFiles(config.build.outDir, config.logger, pagesDir, distDir);
834
+ config.logger.info(green(`✓ copied in ${Date.now() - time}ms`));
701
835
  }
702
836
  };
703
837
  }
704
838
  //#endregion
705
- //#region src/structured-data/website.ts
706
- /**
707
- * @see https://developer.yoast.com/features/schema/pieces/website/
708
- */
709
- function website(structuredData, options) {
710
- return { data: {
711
- "@type": "WebSite",
712
- "@id": joinURL(options.url, "#", "schema", "WebSite", "1"),
713
- "url": options.url,
714
- "name": options.name,
715
- "inLanguage": ["en-US"],
716
- "publisher": { "@id": structuredData.person.data["@id"] }
717
- } };
839
+ //#region src/domain/sitemap.ts
840
+ function generateSitemap(outDir, hostname, routes) {
841
+ const sitemapStream = new SitemapStream({ hostname: `https://${hostname}` });
842
+ const writeStream = createWriteStream(join(outDir, "sitemap.xml"));
843
+ sitemapStream.pipe(writeStream);
844
+ routes.forEach((item) => sitemapStream.write(item));
845
+ sitemapStream.end();
718
846
  }
719
847
  //#endregion
720
- //#region src/structured-data/index.ts
721
- function structuredData(id, frontmatter, options) {
722
- const { name, hostname, extractPage, getPageConfig } = options;
723
- const graph = {
724
- "@context": "https://schema.org",
725
- "@graph": []
726
- };
727
- const structuredDataOptions = {
728
- name,
729
- hostname,
730
- url: toUrl(hostname)
848
+ //#region src/plugins/sitemap.ts
849
+ function sitemap_default(hostname) {
850
+ const routes = /* @__PURE__ */ new Set();
851
+ let config;
852
+ return {
853
+ name: "soubiran:sitemap",
854
+ config() {
855
+ return { ssgOptions: {
856
+ onPageRendered(route, renderedHTML) {
857
+ routes.add(route);
858
+ return renderedHTML;
859
+ },
860
+ onFinished() {
861
+ generateSitemap(config.build.outDir, hostname, Array.from(routes));
862
+ }
863
+ } };
864
+ },
865
+ configResolved(resolvedConfig) {
866
+ config = resolvedConfig;
867
+ }
731
868
  };
732
- const personData = person(structuredDataOptions, options.person);
733
- const websiteData = website({ person: personData }, structuredDataOptions);
734
- const webpageData = webpage(id, { website: websiteData }, {
735
- title: frontmatter.title,
736
- description: frontmatter.description,
737
- datePublished: frontmatter.date ? new Date(frontmatter.date) : void 0,
738
- keywords: frontmatter.tags
739
- }, structuredDataOptions);
740
- const page = extractPage(id);
741
- const pageConfig = getPageConfig?.(page, frontmatter);
742
- if (pageConfig?.type === "article") {
743
- const articleData = article(id, {
744
- person: personData,
745
- webpage: webpageData
746
- }, {
747
- title: frontmatter.title,
748
- description: frontmatter.description
749
- }, structuredDataOptions);
750
- graph["@graph"].push(articleData.data);
751
- if (pageConfig.breadcrumbItems) {
752
- const breadcrumbData = breadcrumb(id, pageConfig.breadcrumbItems, structuredDataOptions);
753
- graph["@graph"].push(breadcrumbData.data);
754
- webpageData.setBreadcrumb(breadcrumbData);
869
+ }
870
+ //#endregion
871
+ //#region src/plugins/ssg.ts
872
+ function ssg_default() {
873
+ return {
874
+ name: "soubiran:ssg",
875
+ config() {
876
+ return { ssgOptions: { formatting: "minify" } };
755
877
  }
756
- } else if (pageConfig?.type === "collection") webpageData.setCollection();
757
- graph["@graph"].push(personData.data, websiteData.data, webpageData.data);
758
- frontmatter.script ??= [];
759
- frontmatter.script.push({
760
- type: "application/ld+json",
761
- innerHTML: JSON.stringify(graph)
762
- });
878
+ };
763
879
  }
764
880
  //#endregion
765
- //#region vite.config.ts
766
- var vite_config_default = (title, hostname, options, config = {}) => {
767
- const seo = { person: {
768
- name: "Estéban Soubiran",
769
- sameAs: [
770
- "https://x.com/soubiran_",
771
- "https://www.linkedin.com/in/esteban25",
772
- "https://www.twitch.tv/barbapapazes",
773
- "https://www.youtube.com/@barbapapazes",
774
- "https://github.com/barbapapazes",
775
- "https://soubiran.dev",
776
- "https://esteban-soubiran.site",
777
- "https://barbapapazes.dev"
778
- ]
779
- } };
780
- return mergeConfig(defineConfig({
781
- plugins: [
782
- vueRouter({
783
- extensions: [".vue", ".md"],
784
- routesFolder: "pages",
785
- dts: "src/typed-router.d.ts",
786
- extendRoute(route) {
787
- const path = route.components.get("default");
788
- if (!path) return;
789
- if (path.endsWith(".vue")) route.addToMeta({ frontmatter: { page: options.extractPage(path) } });
790
- if (path.endsWith(".md")) {
791
- const { data } = matter(readFileSync(path, "utf-8"));
792
- route.addToMeta({ frontmatter: data });
793
- }
881
+ //#region src/index.ts
882
+ function soubiran(title, hostname, options) {
883
+ return [
884
+ options.router === false ? void 0 : router({
885
+ extensions: [".vue", ".md"],
886
+ routesFolder: "pages",
887
+ dts: "src/route-map.d.ts",
888
+ extendRoute(route) {
889
+ const path = route.components.get("default");
890
+ if (!path) return;
891
+ if (path.endsWith(".vue") && options.router && options.router.extractPage) route.addToMeta({ frontmatter: { page: options.router.extractPage(path) } });
892
+ if (path.endsWith(".md")) {
893
+ const { data } = matter(readFileSync(path, "utf-8"));
894
+ route.addToMeta({ frontmatter: data });
794
895
  }
795
- }),
796
- vue({ include: [/\.vue$/, /\.md$/] }),
797
- ui({
798
- autoImport: {
799
- dts: "src/auto-imports.d.ts",
800
- dirs: ["src/composables"],
801
- imports: [
802
- "vue",
803
- "vue-router",
804
- "@vueuse/core",
805
- unheadVueComposablesImports,
806
- {
807
- from: "tailwind-variants",
808
- imports: ["tv"]
809
- },
810
- soubiranComposablesImports
811
- ]
812
- },
813
- components: {
814
- include: [
815
- /\.vue$/,
816
- /\.vue\?vue/,
817
- /\.md$/
818
- ],
819
- dts: "src/components.d.ts",
820
- resolvers: [soubiranResolver()]
821
- },
822
- ui: { colors: { neutral: "neutral" } }
823
- }),
824
- markdown({
825
- headEnabled: true,
826
- wrapperClasses: [
827
- "slide-enter-content",
828
- "max-w-none",
829
- "prose prose-neutral dark:prose-invert",
830
- "prose-headings:text-default prose-h2:text-[1.125em] prose-h2:mb-[0.5em] prose-h3:text-[1em]",
831
- "prose-p:my-[1em] dark:prose-p:text-muted",
832
- "dark:prose-ul:text-muted dark:prose-ol:text-muted",
833
- "dark:prose-strong:text-default",
834
- "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)]",
835
- "prose-hr:max-w-1/2 prose-hr:mx-auto prose-hr:my-[2em]",
836
- "prose-figure:bg-neutral-100 dark:prose-figure:bg-neutral-800 prose-figure:rounded-lg",
837
- "prose-img:rounded-lg prose-img:border prose-img:border-accented prose-img:shadow-md",
838
- "prose-video:rounded-lg prose-video:border prose-video:border-accented prose-video:shadow-md",
839
- "prose-figcaption:text-center prose-figcaption:py-1 prose-figcaption:m-0",
840
- "[&_:first-child]:mt-0 [&_:last-child]:mb-0"
841
- ],
842
- transforms: options.markdown?.transforms ?? {},
843
- wrapperComponent: options.markdown?.wrapperComponent,
844
- async markdownItSetup(md) {
845
- githubAlerts(md);
846
- implicitFiguresRule(md);
847
- linkAttributesRule(md);
848
- tableOfContentsRule(md);
849
- customLink(md, hostname);
850
- customImage(md, hostname);
851
- await shikiHighlight(md);
852
- },
853
- frontmatterPreprocess(frontmatter, frontmatterOptions, id, defaults) {
854
- createAssert(options.seo?.assert?.rules)(id, frontmatter);
855
- og(id, frontmatter, hostname);
856
- canonical(id, frontmatter, hostname);
857
- structuredData(id, frontmatter, {
858
- name: title,
859
- hostname,
860
- person: options.seo?.person ?? seo.person,
861
- extractPage: options.extractPage,
862
- getPageConfig: options.seo?.structuredData?.pageConfig
863
- });
864
- frontmatter.page = options.extractPage(id);
865
- return {
866
- head: defaults(frontmatter, frontmatterOptions),
867
- frontmatter
868
- };
869
- }
870
- }),
871
- fonts({ google: { families: [
872
- {
873
- name: "DM Sans",
874
- styles: "ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000"
875
- },
876
- {
877
- name: "DM Mono",
878
- styles: "ital,wght@0,300;0,400;0,500;1,300;1,400;1,500"
879
- },
880
- {
881
- name: "Sofia Sans",
882
- styles: "ital,wght@0,1..1000;1,1..1000"
883
- }
884
- ] } }),
885
- icons({ autoInstall: true }),
886
- apiPlugin(options.apiCategories),
887
- markdownPlugin(),
888
- metaPlugin(hostname),
896
+ }
897
+ }),
898
+ vue({ include: vueIncludePatterns }),
899
+ ui({
900
+ autoImport: {
901
+ dts: "src/auto-imports.d.ts",
902
+ dirs: ["src/composables"],
903
+ imports: [
904
+ "vue",
905
+ "vue-router",
906
+ "@vueuse/core",
907
+ unheadVueComposablesImports,
908
+ {
909
+ from: "tailwind-variants",
910
+ imports: ["tv"]
911
+ },
912
+ soubiranComposablesImports
913
+ ]
914
+ },
915
+ components: {
916
+ include: componentIncludePatterns,
917
+ dts: "src/components.d.ts",
918
+ resolvers: [soubiranResolver()]
919
+ },
920
+ ui: { colors: { neutral: "neutral" } }
921
+ }),
922
+ options.markdown === false ? void 0 : markdown({
923
+ headEnabled: true,
924
+ wrapperClasses: soubiranWrapperClasses,
925
+ transforms: options.markdown?.options?.transforms ?? {},
926
+ wrapperComponent: options.markdown?.options?.wrapperComponent,
927
+ markdownItSetup: markdownRulesFactory(hostname),
928
+ frontmatterPreprocess: markdownFrontmatterFactory({
929
+ title,
930
+ hostname,
931
+ extractPage: options.markdown.extractPage,
932
+ assertRules: options.seo?.assert?.rules,
933
+ getPageConfig: options.seo?.structuredData?.pageConfig
934
+ })
935
+ }),
936
+ fonts({ google: { families: [
889
937
  {
890
- name: "await",
891
- async closeBundle() {
892
- await resolveAll();
893
- }
938
+ name: "DM Sans",
939
+ styles: "ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000"
894
940
  },
895
941
  {
896
- name: "extract-config",
897
- configResolved(resolvedConfig) {
898
- Object.assign(config, resolvedConfig);
899
- }
900
- }
901
- ],
902
- optimizeDeps: { exclude: ["@soubiran/ui"] },
903
- resolve: { alias: { "@": resolve("./src") } },
904
- ssgOptions: {
905
- formatting: "minify",
906
- onPageRendered(route, renderedHTML) {
907
- routes.add(route);
908
- return renderedHTML;
942
+ name: "DM Mono",
943
+ styles: "ital,wght@0,300;0,400;0,500;1,300;1,400;1,500"
909
944
  },
910
- onFinished() {
911
- sitemap(config, hostname, Array.from(routes));
945
+ {
946
+ name: "Sofia Sans",
947
+ styles: "ital,wght@0,1..1000;1,1..1000"
912
948
  }
913
- }
914
- }), config);
915
- };
949
+ ] } }),
950
+ icons({ autoInstall: true }),
951
+ config_default(),
952
+ ssg_default(),
953
+ meta_default(hostname),
954
+ api_default(options.api?.categories),
955
+ raw_markdown_default(),
956
+ sitemap_default(hostname),
957
+ promise_default()
958
+ ];
959
+ }
916
960
  //#endregion
917
- export { vite_config_default as default };
961
+ export { soubiran as default };