docula 0.40.0 → 0.41.1

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/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - [Building Multiple Pages](#building-multiple-pages)
18
18
  - [Public Folder](#public-folder)
19
19
  - [Announcements](#announcements)
20
+ - [Changelog](#changelog)
20
21
  - [Alert, Info, Warn Styling](#alert-info-warn-styling)
21
22
  - [Using a Github Token](#using-a-github-token)
22
23
  - [Helpers](#helpers)
@@ -272,6 +273,94 @@ You can customize the appearance by overriding the `.announcement` class in your
272
273
 
273
274
  Simply delete the `announcement.md` file when you no longer need the announcement. The home page will automatically return to its normal layout.
274
275
 
276
+ # Changelog
277
+
278
+ Docula can generate a changelog section for your site from markdown files. This is useful for documenting release notes, updates, and changes to your project in a structured, browsable format.
279
+
280
+ ## Setup
281
+
282
+ Create a `changelog` folder inside your site directory and add markdown (`.md` or `.mdx`) files for each entry:
283
+
284
+ ```
285
+ site
286
+ ├───changelog
287
+ │ ├───2025-01-15-initial-release.md
288
+ │ ├───2025-02-01-new-features.md
289
+ │ └───2025-03-10-bug-fixes.md
290
+ ├───logo.svg
291
+ ├───favicon.ico
292
+ └───docula.config.mjs
293
+ ```
294
+
295
+ ## Entry Format
296
+
297
+ Each changelog entry is a markdown file with front matter:
298
+
299
+ ```md
300
+ ---
301
+ title: "Initial Release"
302
+ date: 2025-01-15
303
+ tag: "Release"
304
+ ---
305
+
306
+ We're excited to announce the initial release! Here's what's included:
307
+
308
+ - Feature A
309
+ - Feature B
310
+ - Bug fix C
311
+ ```
312
+
313
+ ### Front Matter Fields
314
+
315
+ | Field | Required | Description |
316
+ |-------|----------|-------------|
317
+ | `title` | No | Display title for the entry. Defaults to the filename if not provided. |
318
+ | `date` | Yes | Date of the entry (`YYYY-MM-DD`). Used for sorting (newest first). |
319
+ | `tag` | No | A label displayed as a badge (e.g., `Release`, `Bug Fix`, `Feature`). Gets a CSS class based on its value for styling. |
320
+
321
+ ## File Naming
322
+
323
+ Files can optionally be prefixed with a date in `YYYY-MM-DD-` format. The date prefix is stripped to create the URL slug:
324
+
325
+ - `2025-01-15-initial-release.md` → `/changelog/initial-release/`
326
+ - `new-features.md` → `/changelog/new-features/`
327
+
328
+ ## Generated Pages
329
+
330
+ When changelog entries are found, Docula generates:
331
+
332
+ - **Changelog listing page** at `/changelog/` — shows all entries sorted by date (newest first) with titles, dates, tags, and content
333
+ - **Individual entry pages** at `/changelog/{slug}/` — a dedicated page for each entry with a back link to the listing
334
+
335
+ Changelog URLs are also automatically added to the generated `sitemap.xml`.
336
+
337
+ ## Styling
338
+
339
+ Tags receive a CSS class based on their value (e.g., a tag of `"Bug Fix"` gets the class `changelog-tag-bug-fix`). You can style tags and other changelog elements by overriding these classes in your `variables.css`:
340
+
341
+ ```css
342
+ .changelog-entry {
343
+ border-bottom: 1px solid var(--border);
344
+ padding: 1.5rem 0;
345
+ }
346
+
347
+ .changelog-tag {
348
+ font-size: 0.75rem;
349
+ padding: 0.2rem 0.5rem;
350
+ border-radius: 4px;
351
+ }
352
+
353
+ .changelog-tag-release {
354
+ background-color: #d4edda;
355
+ color: #155724;
356
+ }
357
+
358
+ .changelog-tag-bug-fix {
359
+ background-color: #f8d7da;
360
+ color: #721c24;
361
+ }
362
+ ```
363
+
275
364
  # Alert, Info, Warn Styling
276
365
 
277
366
  Docula uses Writr's GitHub-flavored Markdown plugins, including GitHub-style blockquote alerts. Use the alert syntax directly in Markdown:
package/dist/docula.d.ts CHANGED
@@ -49,6 +49,12 @@ declare class DoculaOptions {
49
49
  * Sections
50
50
  */
51
51
  sections?: DoculaSection[];
52
+ /**
53
+ * OpenAPI specification URL for API documentation.
54
+ * When provided, creates a dedicated /api page
55
+ * Supports both external URLs (https://...) and relative paths (/openapi.json)
56
+ */
57
+ openApiUrl?: string;
52
58
  constructor(options?: Record<string, unknown>);
53
59
  parseOptions(options: Record<string, any>): void;
54
60
  }
package/dist/docula.js CHANGED
@@ -11,9 +11,7 @@ import updateNotifier from "update-notifier";
11
11
  // src/builder.ts
12
12
  import fs from "fs";
13
13
  import path3 from "path";
14
- import * as cheerio from "cheerio";
15
14
  import { Ecto } from "ecto";
16
- import he from "he";
17
15
  import { Writr } from "writr";
18
16
 
19
17
  // src/console.ts
@@ -291,6 +289,12 @@ var DoculaOptions = class {
291
289
  * Sections
292
290
  */
293
291
  sections;
292
+ /**
293
+ * OpenAPI specification URL for API documentation.
294
+ * When provided, creates a dedicated /api page
295
+ * Supports both external URLs (https://...) and relative paths (/openapi.json)
296
+ */
297
+ openApiUrl;
294
298
  constructor(options) {
295
299
  if (options) {
296
300
  this.parseOptions(options);
@@ -331,6 +335,9 @@ var DoculaOptions = class {
331
335
  if (options.singlePage !== void 0 && typeof options.singlePage === "boolean") {
332
336
  this.singlePage = options.singlePage;
333
337
  }
338
+ if (options.openApiUrl) {
339
+ this.openApiUrl = options.openApiUrl;
340
+ }
334
341
  }
335
342
  };
336
343
 
@@ -360,7 +367,8 @@ var DoculaBuilder = class {
360
367
  templatePath: this.options.templatePath,
361
368
  outputPath: this.options.outputPath,
362
369
  githubPath: this.options.githubPath,
363
- sections: this.options.sections
370
+ sections: this.options.sections,
371
+ openApiUrl: this.options.openApiUrl
364
372
  };
365
373
  doculaData.github = await this.getGithubData(this.options.githubPath);
366
374
  doculaData.documents = this.getDocuments(
@@ -372,9 +380,13 @@ var DoculaBuilder = class {
372
380
  this.options
373
381
  );
374
382
  doculaData.hasDocuments = doculaData.documents?.length > 0;
383
+ const changelogPath = `${doculaData.sitePath}/changelog`;
384
+ doculaData.changelogEntries = this.getChangelogEntries(changelogPath);
385
+ doculaData.hasChangelog = doculaData.changelogEntries.length > 0;
375
386
  doculaData.templates = await this.getTemplates(
376
387
  this.options,
377
- doculaData.hasDocuments
388
+ doculaData.hasDocuments,
389
+ doculaData.hasChangelog
378
390
  );
379
391
  await this.buildIndexPage(doculaData);
380
392
  await this.buildReleasePage(doculaData);
@@ -383,6 +395,13 @@ var DoculaBuilder = class {
383
395
  if (doculaData.hasDocuments) {
384
396
  await this.buildDocsPages(doculaData);
385
397
  }
398
+ if (doculaData.openApiUrl) {
399
+ await this.buildApiPage(doculaData);
400
+ }
401
+ if (doculaData.hasChangelog) {
402
+ await this.buildChangelogPage(doculaData);
403
+ await this.buildChangelogEntryPages(doculaData);
404
+ }
386
405
  const siteRelativePath = this.options.sitePath;
387
406
  if (fs.existsSync(`${siteRelativePath}/favicon.ico`)) {
388
407
  await fs.promises.copyFile(
@@ -442,7 +461,7 @@ var DoculaBuilder = class {
442
461
  const github = new Github(options);
443
462
  return github.getData();
444
463
  }
445
- async getTemplates(options, hasDocuments) {
464
+ async getTemplates(options, hasDocuments, hasChangelog = false) {
446
465
  const templates = {
447
466
  index: "",
448
467
  releases: ""
@@ -463,6 +482,18 @@ var DoculaBuilder = class {
463
482
  if (documentPage) {
464
483
  templates.docPage = documentPage;
465
484
  }
485
+ const apiPage = options.openApiUrl ? await this.getTemplateFile(options.templatePath, "api") : void 0;
486
+ if (apiPage) {
487
+ templates.api = apiPage;
488
+ }
489
+ const changelogPage = hasChangelog ? await this.getTemplateFile(options.templatePath, "changelog") : void 0;
490
+ if (changelogPage) {
491
+ templates.changelog = changelogPage;
492
+ }
493
+ const changelogEntryPage = hasChangelog ? await this.getTemplateFile(options.templatePath, "changelog-entry") : void 0;
494
+ if (changelogEntryPage) {
495
+ templates.changelogEntry = changelogEntryPage;
496
+ }
466
497
  } else {
467
498
  throw new Error(`No template path found at ${options.templatePath}`);
468
499
  }
@@ -490,6 +521,17 @@ var DoculaBuilder = class {
490
521
  async buildSiteMapPage(data) {
491
522
  const sitemapPath = `${data.outputPath}/sitemap.xml`;
492
523
  const urls = [{ url: data.siteUrl }, { url: `${data.siteUrl}/releases` }];
524
+ if (data.openApiUrl && data.templates?.api) {
525
+ urls.push({ url: `${data.siteUrl}/api` });
526
+ }
527
+ if (data.hasChangelog && data.templates?.changelog) {
528
+ urls.push({ url: `${data.siteUrl}/changelog` });
529
+ for (const entry of data.changelogEntries ?? []) {
530
+ urls.push({
531
+ url: `${data.siteUrl}/changelog/${entry.slug}`
532
+ });
533
+ }
534
+ }
493
535
  for (const document of data.documents ?? []) {
494
536
  let { urlPath } = document;
495
537
  if (urlPath.endsWith("index.html")) {
@@ -574,12 +616,11 @@ var DoculaBuilder = class {
574
616
  recursive: true
575
617
  });
576
618
  const slug = `${data.outputPath}${document.urlPath}`;
577
- let documentContent = await this._ecto.renderFromFile(
619
+ const documentContent = await this._ecto.renderFromFile(
578
620
  documentsTemplate,
579
621
  { ...data, ...document },
580
622
  data.templatePath
581
623
  );
582
- documentContent = he.decode(documentContent);
583
624
  return fs.promises.writeFile(slug, documentContent, "utf8");
584
625
  });
585
626
  await Promise.all(promises);
@@ -587,6 +628,121 @@ var DoculaBuilder = class {
587
628
  throw new Error("No templates found");
588
629
  }
589
630
  }
631
+ async buildApiPage(data) {
632
+ if (!data.openApiUrl || !data.templates?.api) {
633
+ return;
634
+ }
635
+ const apiPath = `${data.outputPath}/api/index.html`;
636
+ const apiOutputPath = `${data.outputPath}/api`;
637
+ await fs.promises.mkdir(apiOutputPath, { recursive: true });
638
+ const apiTemplate = `${data.templatePath}/${data.templates.api}`;
639
+ const apiContent = await this._ecto.renderFromFile(
640
+ apiTemplate,
641
+ { ...data, specUrl: data.openApiUrl },
642
+ data.templatePath
643
+ );
644
+ await fs.promises.writeFile(apiPath, apiContent, "utf8");
645
+ }
646
+ getChangelogEntries(changelogPath) {
647
+ const entries = [];
648
+ if (!fs.existsSync(changelogPath)) {
649
+ return entries;
650
+ }
651
+ const files = fs.readdirSync(changelogPath);
652
+ for (const file of files) {
653
+ const filePath = `${changelogPath}/${file}`;
654
+ const stats = fs.statSync(filePath);
655
+ if (stats.isFile() && (file.endsWith(".md") || file.endsWith(".mdx"))) {
656
+ const entry = this.parseChangelogEntry(filePath);
657
+ entries.push(entry);
658
+ }
659
+ }
660
+ entries.sort((a, b) => {
661
+ const dateA = new Date(a.date).getTime();
662
+ const dateB = new Date(b.date).getTime();
663
+ if (Number.isNaN(dateA) && Number.isNaN(dateB)) {
664
+ return 0;
665
+ }
666
+ if (Number.isNaN(dateA)) {
667
+ return 1;
668
+ }
669
+ if (Number.isNaN(dateB)) {
670
+ return -1;
671
+ }
672
+ return dateB - dateA;
673
+ });
674
+ return entries;
675
+ }
676
+ parseChangelogEntry(filePath) {
677
+ const fileContent = fs.readFileSync(filePath, "utf8");
678
+ const writr = new Writr(fileContent);
679
+ const matterData = writr.frontMatter;
680
+ const markdownContent = writr.body;
681
+ const fileName = path3.basename(filePath, path3.extname(filePath));
682
+ const slug = fileName.replace(/^\d{4}-\d{2}-\d{2}-/, "");
683
+ const isMdx = filePath.endsWith(".mdx");
684
+ const tag = matterData.tag;
685
+ const tagClass = tag ? tag.toLowerCase().replace(/\s+/g, "-") : void 0;
686
+ let dateString = "";
687
+ if (matterData.date instanceof Date) {
688
+ dateString = matterData.date.toISOString().split("T")[0];
689
+ } else if (matterData.date) {
690
+ dateString = String(matterData.date);
691
+ }
692
+ let formattedDate = dateString;
693
+ const parsedDate = new Date(dateString);
694
+ if (!Number.isNaN(parsedDate.getTime())) {
695
+ formattedDate = parsedDate.toLocaleDateString("en-US", {
696
+ year: "numeric",
697
+ month: "long",
698
+ day: "numeric"
699
+ });
700
+ }
701
+ return {
702
+ title: matterData.title ?? fileName,
703
+ date: dateString,
704
+ formattedDate,
705
+ tag,
706
+ tagClass,
707
+ slug,
708
+ content: markdownContent,
709
+ generatedHtml: new Writr(markdownContent).renderSync({ mdx: isMdx }),
710
+ urlPath: `/changelog/${slug}/index.html`
711
+ };
712
+ }
713
+ async buildChangelogPage(data) {
714
+ if (!data.hasChangelog || !data.templates?.changelog) {
715
+ return;
716
+ }
717
+ const changelogOutputPath = `${data.outputPath}/changelog`;
718
+ const changelogIndexPath = `${changelogOutputPath}/index.html`;
719
+ await fs.promises.mkdir(changelogOutputPath, { recursive: true });
720
+ const changelogTemplate = `${data.templatePath}/${data.templates.changelog}`;
721
+ const changelogContent = await this._ecto.renderFromFile(
722
+ changelogTemplate,
723
+ { ...data, entries: data.changelogEntries },
724
+ data.templatePath
725
+ );
726
+ await fs.promises.writeFile(changelogIndexPath, changelogContent, "utf8");
727
+ }
728
+ async buildChangelogEntryPages(data) {
729
+ if (!data.hasChangelog || !data.templates?.changelogEntry || !data.changelogEntries?.length) {
730
+ return;
731
+ }
732
+ const entryTemplate = `${data.templatePath}/${data.templates.changelogEntry}`;
733
+ const promises = data.changelogEntries.map(async (entry) => {
734
+ const entryOutputPath = `${data.outputPath}/changelog/${entry.slug}`;
735
+ await fs.promises.mkdir(entryOutputPath, { recursive: true });
736
+ const entryContent = await this._ecto.renderFromFile(
737
+ entryTemplate,
738
+ { ...data, ...entry, entries: data.changelogEntries },
739
+ data.templatePath
740
+ );
741
+ const entryFilePath = `${entryOutputPath}/index.html`;
742
+ return fs.promises.writeFile(entryFilePath, entryContent, "utf8");
743
+ });
744
+ await Promise.all(promises);
745
+ }
590
746
  generateSidebarItems(data) {
591
747
  let sidebarItems = [...data.sections ?? []];
592
748
  for (const document of data.documents ?? []) {
@@ -723,6 +879,11 @@ var DoculaBuilder = class {
723
879
  urlPath = documentPath.slice(documentsFolderIndex).replace(fileExtension, ".html");
724
880
  }
725
881
  }
882
+ if (!this.hasTableOfContents(markdownContent)) {
883
+ markdownContent = `## Table of Contents
884
+
885
+ ${markdownContent}`;
886
+ }
726
887
  return {
727
888
  title: matterData.title,
728
889
  navTitle: matterData.navTitle ?? matterData.title,
@@ -732,29 +893,15 @@ var DoculaBuilder = class {
732
893
  keywords: matterData.keywords ?? [],
733
894
  content: documentContent,
734
895
  markdown: markdownContent,
735
- generatedHtml: new Writr(markdownContent).renderSync({ mdx: isMdx }),
736
- tableOfContents: this.getTableOfContents(markdownContent, isMdx),
896
+ generatedHtml: new Writr(markdownContent).renderSync({
897
+ toc: true,
898
+ mdx: isMdx
899
+ }),
737
900
  documentPath,
738
901
  urlPath,
739
902
  isRoot
740
903
  };
741
904
  }
742
- getTableOfContents(markdown, isMdx = false) {
743
- if (this.hasTableOfContents(markdown)) {
744
- return void 0;
745
- }
746
- markdown = `## Table of Contents
747
-
748
- ${markdown}`;
749
- const html = new Writr(markdown).renderSync({ mdx: isMdx });
750
- const $ = cheerio.load(html);
751
- const tocTitle = $("h2").first();
752
- const tocContent = tocTitle.next("ul").toString();
753
- if (tocContent) {
754
- return tocTitle.toString() + tocContent;
755
- }
756
- return void 0;
757
- }
758
905
  hasTableOfContents(markdown) {
759
906
  const normalized = markdown.replace(/\r\n/g, "\n");
760
907
  const atxHeading = /^#{1,6}\s*(table of contents|toc)\s*$/im;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docula",
3
- "version": "0.40.0",
3
+ "version": "0.41.1",
4
4
  "description": "Beautiful Website for Your Projects",
5
5
  "type": "module",
6
6
  "main": "./dist/docula.js",
@@ -11,26 +11,15 @@
11
11
  "import": "./dist/docula.js"
12
12
  }
13
13
  },
14
- "repository": "https://github.com/jaredwray/docula.git",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/jaredwray/docula.git"
17
+ },
15
18
  "author": "Jared Wray <me@jaredwray.com>",
16
19
  "engines": {
17
20
  "node": ">=20"
18
21
  },
19
22
  "license": "MIT",
20
- "scripts": {
21
- "clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./pnpm-lock.yaml ./site/README.md ./site/dist",
22
- "build": "pnpm generate-init-file && rimraf ./dist && tsup src/docula.ts --format esm --dts --clean",
23
- "lint": "biome check --write --error-on-warnings",
24
- "lint:ci": "biome check --error-on-warnings",
25
- "test": "pnpm lint && vitest run --coverage",
26
- "test:ci": "pnpm lint:ci && vitest run --coverage",
27
- "generate-init-file": "tsx scripts/generate-init-file.ts",
28
- "website:build": "rimraf ./site/README.md && node bin/docula.mjs build -s ./site -o ./site/dist",
29
- "website:serve": "rimraf ./site/README.md && node bin/docula.mjs serve -s ./site -o ./site/dist",
30
- "website:build:mega": "rimraf ./test/fixtures/mega-page-site/dist && node bin/docula.mjs build -s ./test/fixtures/mega-page-site",
31
- "website:serve:mega": "rimraf ./test/fixtures/mega-page-site/dist && node bin/docula.mjs serve -s ./test/fixtures/mega-page-site",
32
- "prepare": "pnpm build"
33
- },
34
23
  "keywords": [
35
24
  "static-site-generator",
36
25
  "static-site",
@@ -48,38 +37,50 @@
48
37
  "handlebars"
49
38
  ],
50
39
  "bin": {
51
- "docula": "./bin/docula.mjs"
40
+ "docula": "./bin/docula.js"
52
41
  },
53
42
  "dependencies": {
54
- "@cacheable/net": "^2.0.4",
55
- "cheerio": "^1.1.2",
56
- "ecto": "^4.7.1",
43
+ "@cacheable/net": "^2.0.5",
44
+ "ecto": "^4.8.2",
57
45
  "feed": "^5.2.0",
58
- "he": "^1.2.0",
59
46
  "jiti": "^2.6.1",
60
47
  "serve-handler": "^6.1.6",
61
48
  "update-notifier": "^7.3.1",
62
- "writr": "^5.0.1"
49
+ "writr": "^5.0.3"
63
50
  },
64
51
  "devDependencies": {
65
- "@biomejs/biome": "^2.3.9",
52
+ "@biomejs/biome": "^2.4.2",
66
53
  "@types/express": "^5.0.6",
67
- "@types/he": "^1.2.3",
68
54
  "@types/js-yaml": "^4.0.9",
69
- "@types/node": "^25.0.3",
55
+ "@types/node": "^25.2.3",
70
56
  "@types/serve-handler": "^6.1.4",
71
57
  "@types/update-notifier": "^6.0.8",
72
- "@vitest/coverage-v8": "^4.0.16",
73
- "dotenv": "^17.2.3",
74
- "rimraf": "^6.1.2",
58
+ "@vitest/coverage-v8": "^4.0.18",
59
+ "dotenv": "^17.3.1",
60
+ "rimraf": "^6.1.3",
75
61
  "tsup": "^8.5.1",
76
62
  "tsx": "^4.21.0",
77
63
  "typescript": "^5.9.3",
78
- "vitest": "^4.0.16"
64
+ "vitest": "^4.0.18"
79
65
  },
80
66
  "files": [
81
67
  "dist",
82
68
  "template",
83
69
  "bin"
84
- ]
85
- }
70
+ ],
71
+ "scripts": {
72
+ "clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./pnpm-lock.yaml ./site/README.md ./site/dist",
73
+ "build": "pnpm generate-init-file && rimraf ./dist && tsup src/docula.ts --format esm --dts --clean",
74
+ "lint": "biome check --write --error-on-warnings",
75
+ "lint:ci": "biome check --error-on-warnings",
76
+ "test": "pnpm lint && vitest run --coverage",
77
+ "test:ci": "pnpm lint:ci && vitest run --coverage",
78
+ "generate-init-file": "tsx scripts/generate-init-file.ts",
79
+ "website:build": "rimraf ./site/README.md && node bin/docula.js build -s ./site -o ./site/dist",
80
+ "website:serve": "rimraf ./site/README.md && node bin/docula.js serve -s ./site -o ./site/dist",
81
+ "website:build:mega": "rimraf ./test/fixtures/mega-page-site/dist && node bin/docula.js build -s ./test/fixtures/mega-page-site",
82
+ "website:serve:mega": "rimraf ./test/fixtures/mega-page-site/dist && node bin/docula.js serve -s ./test/fixtures/mega-page-site",
83
+ "website:build:changelog": "rimraf ./test/fixtures/changelog-site/dist && node bin/docula.js build -s ./test/fixtures/changelog-site",
84
+ "website:serve:changelog": "rimraf ./test/fixtures/changelog-site/dist && node bin/docula.js serve -s ./test/fixtures/changelog-site"
85
+ }
86
+ }
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ {{> header }}
6
+ <title>API Documentation - {{ siteTitle }}</title>
7
+ <meta name="description" content="API Documentation for {{ siteTitle }}" />
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@docutopia/react/dist/browser/docutopia.css" />
9
+ <style>
10
+ body {
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+ </style>
15
+ </head>
16
+
17
+ <body>
18
+ <div id="docs" style="height: 100vh;"></div>
19
+
20
+ <script src="https://cdn.jsdelivr.net/npm/@docutopia/react/dist/browser/docutopia.js"></script>
21
+ <script>
22
+ Docutopia.render('docs', {
23
+ specUrl: '{{ specUrl }}',
24
+ });
25
+ </script>
26
+ </body>
27
+
28
+ </html>
@@ -0,0 +1,79 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ {{> header }}
6
+ <title>{{siteTitle}} - {{title}}</title>
7
+ </head>
8
+
9
+ <body>
10
+ {{> singlepage/hero }}
11
+ <a href="https://github.com/{{ githubPath }}" class="github-corner" aria-label="View source on GitHub"><svg width="80"
12
+ height="80" viewBox="0 0 250 250" style="color:#fff; position: absolute; top: 0; border: 0; right: 0;"
13
+ aria-hidden="true">
14
+ <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
15
+ <path
16
+ d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
17
+ fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
18
+ <path
19
+ d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
20
+ fill="currentColor" class="octo-body"></path>
21
+ </svg></a>
22
+ <style>
23
+ .github-corner:hover .octo-arm {
24
+ animation: octocat-wave 560ms ease-in-out
25
+ }
26
+
27
+ @keyframes octocat-wave {
28
+
29
+ 0%,
30
+ 100% {
31
+ transform: rotate(0)
32
+ }
33
+
34
+ 20%,
35
+ 60% {
36
+ transform: rotate(-25deg)
37
+ }
38
+
39
+ 40%,
40
+ 80% {
41
+ transform: rotate(10deg)
42
+ }
43
+ }
44
+
45
+ @media (max-width:500px) {
46
+ .github-corner:hover .octo-arm {
47
+ animation: none
48
+ }
49
+
50
+ .github-corner .octo-arm {
51
+ animation: octocat-wave 560ms ease-in-out
52
+ }
53
+ }
54
+ </style>
55
+ <main class="versions-container">
56
+ <div class="versions-content">
57
+ <div class="changelog-entry-nav">
58
+ <a href="/changelog/">&larr; Back to Changelog</a>
59
+ </div>
60
+ <div class="changelog-entry changelog-entry-single">
61
+ <div class="changelog-entry-header">
62
+ <h1 class="changelog-entry-title">{{title}}</h1>
63
+ {{#if tag}}
64
+ <span class="changelog-tag changelog-tag-{{tagClass}}">{{tag}}</span>
65
+ {{/if}}
66
+ </div>
67
+ <span class="changelog-entry-date">{{formattedDate}}</span>
68
+ <div class="changelog-entry-body">
69
+ {{{generatedHtml}}}
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </main>
74
+ {{> footer}}
75
+
76
+ {{> scripts }}
77
+ </body>
78
+
79
+ </html>
@@ -0,0 +1,81 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ {{> header }}
6
+ <title>{{siteTitle}} Changelog</title>
7
+ </head>
8
+
9
+ <body>
10
+ {{> singlepage/hero }}
11
+ <a href="https://github.com/{{ githubPath }}" class="github-corner" aria-label="View source on GitHub"><svg width="80"
12
+ height="80" viewBox="0 0 250 250" style="color:#fff; position: absolute; top: 0; border: 0; right: 0;"
13
+ aria-hidden="true">
14
+ <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
15
+ <path
16
+ d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
17
+ fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
18
+ <path
19
+ d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
20
+ fill="currentColor" class="octo-body"></path>
21
+ </svg></a>
22
+ <style>
23
+ .github-corner:hover .octo-arm {
24
+ animation: octocat-wave 560ms ease-in-out
25
+ }
26
+
27
+ @keyframes octocat-wave {
28
+
29
+ 0%,
30
+ 100% {
31
+ transform: rotate(0)
32
+ }
33
+
34
+ 20%,
35
+ 60% {
36
+ transform: rotate(-25deg)
37
+ }
38
+
39
+ 40%,
40
+ 80% {
41
+ transform: rotate(10deg)
42
+ }
43
+ }
44
+
45
+ @media (max-width:500px) {
46
+ .github-corner:hover .octo-arm {
47
+ animation: none
48
+ }
49
+
50
+ .github-corner .octo-arm {
51
+ animation: octocat-wave 560ms ease-in-out
52
+ }
53
+ }
54
+ </style>
55
+ <main class="versions-container">
56
+ <div class="versions-content">
57
+ <h2 class="home-title">Changelog</h2>
58
+ {{#if entries}}
59
+ {{#each entries as |entry|}}
60
+ <div class="changelog-entry">
61
+ <div class="changelog-entry-header">
62
+ <a class="changelog-entry-title" href="/changelog/{{entry.slug}}/">{{entry.title}}</a>
63
+ {{#if entry.tag}}
64
+ <span class="changelog-tag changelog-tag-{{entry.tagClass}}">{{entry.tag}}</span>
65
+ {{/if}}
66
+ </div>
67
+ <span class="changelog-entry-date">{{entry.formattedDate}}</span>
68
+ <div class="changelog-entry-body">
69
+ {{{entry.generatedHtml}}}
70
+ </div>
71
+ </div>
72
+ {{/each}}
73
+ {{/if}}
74
+ </div>
75
+ </main>
76
+ {{> footer}}
77
+
78
+ {{> scripts }}
79
+ </body>
80
+
81
+ </html>
@@ -193,6 +193,140 @@ hr {
193
193
  text-decoration: underline;
194
194
  }
195
195
 
196
+ /* Changelog */
197
+ .changelog-entry {
198
+ overflow: hidden;
199
+ width: 100%;
200
+ line-break: anywhere;
201
+ margin-top: 2rem;
202
+ padding-bottom: 2rem;
203
+ border-bottom: 1px solid var(--border);
204
+ color: var(--color-text);
205
+ }
206
+
207
+ .changelog-entry:last-child {
208
+ border-bottom: none;
209
+ }
210
+
211
+ .changelog-entry-header {
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 0.75rem;
215
+ margin-bottom: 0.5rem;
216
+ flex-wrap: wrap;
217
+ }
218
+
219
+ .changelog-entry-title {
220
+ font-size: 1.375rem;
221
+ color: var(--color-primary);
222
+ font-weight: 700;
223
+ transition: color .3s;
224
+ }
225
+
226
+ a.changelog-entry-title:hover {
227
+ color: var(--color-secondary-dark);
228
+ }
229
+
230
+ h1.changelog-entry-title {
231
+ font-size: 1.875rem;
232
+ margin: 0;
233
+ }
234
+
235
+ .changelog-entry-date {
236
+ font-size: 0.75rem;
237
+ display: block;
238
+ margin-bottom: 1rem;
239
+ }
240
+
241
+ .changelog-tag {
242
+ display: inline-block;
243
+ padding: 0.2rem 0.6rem;
244
+ border-radius: 1rem;
245
+ font-size: 0.7rem;
246
+ font-weight: 600;
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.025em;
249
+ }
250
+
251
+ .changelog-tag-added {
252
+ background-color: rgba(140, 220, 0, 0.15);
253
+ color: #4a7a00;
254
+ }
255
+
256
+ .changelog-tag-improved {
257
+ background-color: rgba(59, 130, 246, 0.15);
258
+ color: #1d4ed8;
259
+ }
260
+
261
+ .changelog-tag-fixed {
262
+ background-color: rgba(245, 158, 11, 0.15);
263
+ color: #b45309;
264
+ }
265
+
266
+ .changelog-tag-removed {
267
+ background-color: rgba(239, 68, 68, 0.15);
268
+ color: #b91c1c;
269
+ }
270
+
271
+ .changelog-tag-deprecated {
272
+ background-color: rgba(156, 163, 175, 0.15);
273
+ color: #4b5563;
274
+ }
275
+
276
+ .changelog-tag-security {
277
+ background-color: rgba(168, 85, 247, 0.15);
278
+ color: #6d28d9;
279
+ }
280
+
281
+ .changelog-entry-body p {
282
+ margin: 0.5rem 0;
283
+ }
284
+
285
+ .changelog-entry-body pre {
286
+ margin-bottom: 2rem;
287
+ }
288
+
289
+ .changelog-entry-body h1 {
290
+ font-size: 1.375rem;
291
+ margin-bottom: 1rem;
292
+ }
293
+
294
+ .changelog-entry-body h2 {
295
+ font-size: 1.125rem;
296
+ margin-top: 1.75rem;
297
+ margin-bottom: 1rem;
298
+ }
299
+
300
+ .changelog-entry-body ul {
301
+ padding-left: 1rem;
302
+ }
303
+
304
+ .changelog-entry-body ul > li {
305
+ margin-bottom: 0.75rem;
306
+ }
307
+
308
+ .changelog-entry-body ul > li a {
309
+ text-decoration: underline;
310
+ }
311
+
312
+ .changelog-entry-nav {
313
+ margin-bottom: 1.5rem;
314
+ }
315
+
316
+ .changelog-entry-nav a {
317
+ color: var(--color-primary);
318
+ font-size: 0.875rem;
319
+ transition: color .3s;
320
+ }
321
+
322
+ .changelog-entry-nav a:hover {
323
+ color: var(--color-secondary-dark);
324
+ }
325
+
326
+ .changelog-entry-single {
327
+ border-bottom: none;
328
+ }
329
+
196
330
  .github-corner {
197
331
  fill: var(--color-primary) !important;
198
332
  }
@@ -378,6 +512,9 @@ footer img {
378
512
  .release {
379
513
  margin-bottom: 4rem;
380
514
  }
515
+ .changelog-entry {
516
+ margin-bottom: 2rem;
517
+ }
381
518
  .versions-container {
382
519
  margin-bottom: 4.5rem;
383
520
  }
@@ -417,6 +554,9 @@ footer img {
417
554
  .content h1 {
418
555
  font-size: 1.9rem;
419
556
  }
557
+ h1.changelog-entry-title {
558
+ font-size: 2.25rem;
559
+ }
420
560
  .content > ul, .content > ol {
421
561
  padding-left: 2rem;
422
562
  }
@@ -241,10 +241,6 @@ details > summary {
241
241
  margin-right: 2rem
242
242
  }
243
243
 
244
- .main-toc {
245
- padding-bottom: 1.5rem;
246
- }
247
-
248
244
  .toc {
249
245
  font-size: 0.875rem;
250
246
  }
@@ -287,18 +283,6 @@ details > summary {
287
283
  text-decoration: none;
288
284
  }
289
285
 
290
- .main-toc ul > li > a::after {
291
- position: absolute;
292
- right: 0;
293
- content: '';
294
- display: inline-block;
295
- width: 0.25rem;
296
- height: 0.25rem;
297
- border-right: 1px solid var(--color-primary);
298
- border-bottom: 1px solid var(--color-primary);
299
- transform: rotate(-45deg);
300
- }
301
-
302
286
  .fixed-toc {
303
287
  display: none;
304
288
  position: fixed;
@@ -2,16 +2,22 @@
2
2
  <div class="main-content">
3
3
  <article class="content">
4
4
  <h1>{{title}}</h1>
5
- <div class="toc main-toc">
6
- {{tableOfContents}}
7
- </div>
8
5
  {{generatedHtml}}
9
6
  </article>
10
7
  <div class="fixed-toc-container">
11
8
  <div class="toc fixed-toc" id="fixed-toc">
12
- {{tableOfContents}}
13
9
  </div>
14
10
  </div>
15
11
  </div>
16
12
 
17
- </div>
13
+ </div>
14
+ <script>
15
+ const tocHeading = document.getElementById('table-of-contents');
16
+ if (tocHeading) {
17
+ const tocList = tocHeading.nextElementSibling;
18
+ const fixedToc = document.getElementById('fixed-toc');
19
+ if (tocList && tocList.tagName === 'UL' && fixedToc) {
20
+ fixedToc.innerHTML = tocList.outerHTML;
21
+ }
22
+ }
23
+ </script>
File without changes