GitHub Well-Architected
@@ -107,7 +107,7 @@ Deploy GitHub with confidence<
Application Security
@@ -330,4 +330,4 @@
+ {{ if .Page.Pages }}
+ {{ range .Page.Pages }}
+
{{ .LinkTitle }}
+ {{ end }}
+ {{ else }}
+ {{ range .Page.Parent.Pages }}
+ {{ if ne .RelPermalink $.Page.RelPermalink }}
+
{{ .LinkTitle }}
+ {{ end }}
+ {{ end }}
{{ end }}
-
\ No newline at end of file
+
diff --git a/package.json b/package.json
index 2b53a48..55a4853 100644
--- a/package.json
+++ b/package.json
@@ -12,8 +12,10 @@
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"watch": "webpack --mode development --watch",
- "dev": "concurrently \"npm run watch\" \"hugo server --disableFastRender --noHTTPCache --watch --navigateToChanged\"",
- "dev:ignore-cache": "concurrently \"npm run watch\" \"hugo server --ignoreCache\""
+ "preprocess:content": "node src/markdown-includer/includeMarkdown.js ./content ./content-processed && node src/resolve-hugo-links/adjust-hugo-links.js ./content-processed",
+ "watch:markdown": "node watch-markdown.js",
+ "dev": "concurrently \"npm run watch\" \"npm run watch:markdown\" \"hugo server --disableFastRender --noHTTPCache --watch --navigateToChanged\"",
+ "dev:ignore-cache": "concurrently \"npm run watch\" \"npm run watch:markdown\" \"hugo server --ignoreCache\""
},
"repository": {
"type": "git",
diff --git a/src/markdown-includer/includeMarkdown.js b/src/markdown-includer/includeMarkdown.js
new file mode 100644
index 0000000..93dd8ed
--- /dev/null
+++ b/src/markdown-includer/includeMarkdown.js
@@ -0,0 +1,213 @@
+import { promises as fs } from 'fs';
+import { fileURLToPath } from 'url';
+import path from 'path';
+
+/**
+ * Check whether the output file already exists.
+ */
+export async function mainMarkdownExists(mainMarkdown) {
+ try {
+ await fs.access(mainMarkdown);
+ return true;
+ } catch (err) {
+ if (err && err.code === 'ENOENT') {
+ return false;
+ }
+ throw err;
+ }
+}
+
+/**
+ * Merge a single Markdown file into the template at outputPath.
+ * The template must contain
relative/or/absolute/path.md.
+ * If sourceDir is provided, reads from source based on relative path and writes to output.
+ */
+export async function includeMarkdownFiles(outputPath, sourceBaseDir = null, outputBaseDir = null) {
+ const marker = '
';
+ const endMarker = '';
+
+ // Determine read path and output path
+ let readPath = outputPath;
+ if (sourceBaseDir && outputBaseDir) {
+ // Compute relative path within output directory
+ const relPath = path.relative(outputBaseDir, outputPath);
+ readPath = path.resolve(sourceBaseDir, relPath);
+ }
+
+ // Only add markdown when the marker is present. If there's no
+ // existing file or the marker is missing, leave the file unchanged.
+ const exists = await mainMarkdownExists(readPath);
+ if (!exists) {
+ console.log(`File ${readPath} does not exist`);
+ return false;
+ }
+
+ let existing = await fs.readFile(readPath, 'utf8');
+ if (!existing.includes(marker)) {
+ console.log(`No ${marker} marker found in ${readPath}`);
+ return false;
+ }
+
+ console.log(`Found ${marker} marker in ${readPath}`);
+ let current = existing;
+ let didMerge = false;
+ let searchFrom = 0;
+
+ while (true) {
+ const startIndex = current.indexOf(marker, searchFrom);
+ if (startIndex === -1) {
+ break;
+ }
+
+ const endIndex = current.indexOf(endMarker, startIndex + marker.length);
+ if (endIndex === -1) {
+ console.log(`No closing ${endMarker} marker found in ${readPath}`);
+ break;
+ }
+
+ const inner = current.slice(startIndex + marker.length, endIndex).trim();
+ console.log(`Inner value between ${marker} and ${endMarker} in ${readPath}: ${inner}`);
+
+ const includePath = path.resolve(path.dirname(readPath), inner);
+ const includedContent = await getIncludedMarkdownContent(includePath, sourceBaseDir, outputBaseDir, readPath);
+ const before = current.slice(0, startIndex);
+ const after = current.slice(endIndex + endMarker.length);
+ current = `${before}${includedContent}${after}`;
+ didMerge = true;
+ searchFrom = before.length + includedContent.length;
+ }
+
+ if (!didMerge) {
+ return false;
+ }
+
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
+ await fs.writeFile(outputPath, current, 'utf8');
+ return true;
+}
+
+async function getIncludedMarkdownContent(targetPath, sourceBaseDir = null, outputBaseDir = null, contextReadPath = null) {
+ // Adjust targetPath if we're reading from source base
+ let readPath = targetPath;
+ if (sourceBaseDir && outputBaseDir && contextReadPath) {
+ // The targetPath is computed from readPath (source), so we can use it as-is
+ readPath = targetPath;
+ }
+
+ const stats = await fs.stat(readPath);
+
+ if (stats.isFile()) {
+ return fs.readFile(readPath, 'utf8');
+ }
+
+ if (!stats.isDirectory()) {
+ return '';
+ }
+
+ const markdownFiles = (await getMarkdownFiles(readPath)).sort((left, right) => left.localeCompare(right));
+ const contents = await Promise.all(markdownFiles.map((markdownFile) => fs.readFile(markdownFile, 'utf8')));
+ return contents.join('\n\n');
+}
+
+async function getMarkdownFiles(targetPath) {
+ const stats = await fs.stat(targetPath);
+
+ if (stats.isFile()) {
+ return path.extname(targetPath).toLowerCase() === '.md' ? [targetPath] : [];
+ }
+
+ if (!stats.isDirectory()) {
+ return [];
+ }
+
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
+ const markdownFiles = [];
+
+ for (const entry of entries) {
+ const fullPath = path.join(targetPath, entry.name);
+
+ if (entry.isDirectory()) {
+ markdownFiles.push(...await getMarkdownFiles(fullPath));
+ continue;
+ }
+
+ if (entry.isFile() && path.extname(entry.name).toLowerCase() === '.md') {
+ markdownFiles.push(fullPath);
+ }
+ }
+
+ return markdownFiles;
+}
+
+export async function includeMarkdownInDirectory(sourcePath, outputPath = null) {
+ const sourceDir = outputPath ? sourcePath : null;
+ const workDir = outputPath || sourcePath;
+
+ // Resolve to absolute paths for proper path resolution
+ const sourceAbsolute = path.resolve(sourceDir || workDir);
+ const outputAbsolute = path.resolve(workDir);
+
+ // If outputPath is specified, copy the directory structure first
+ if (outputPath) {
+ console.log(`Copying ${sourcePath} to ${outputPath}...`);
+ await copyDirectory(sourcePath, outputPath);
+ }
+
+ const markdownFiles = await getMarkdownFiles(workDir);
+ let mergedCount = 0;
+
+ for (const markdownFile of markdownFiles) {
+ const didMerge = await includeMarkdownFiles(markdownFile, sourceAbsolute, outputAbsolute);
+ if (didMerge) {
+ console.log(`Merged content into ${markdownFile}`);
+ mergedCount += 1;
+ }
+ }
+
+ return mergedCount;
+}
+
+async function copyDirectory(source, dest) {
+ await fs.mkdir(dest, { recursive: true });
+ const entries = await fs.readdir(source, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const sourcePath = path.join(source, entry.name);
+ const destPath = path.join(dest, entry.name);
+
+ if (entry.isDirectory()) {
+ await copyDirectory(sourcePath, destPath);
+ } else {
+ await fs.copyFile(sourcePath, destPath);
+ }
+ }
+}
+
+async function main(argv) {
+ const args = argv.slice(2);
+
+ if (args.length < 1) {
+ console.error('Usage: node includeMarkdown.js
[output-path]');
+ console.error(' If output-path is provided, source is copied and processed there');
+ console.error(' If output-path is omitted, source is processed in-place');
+ process.exit(1);
+ }
+
+ // Resolve paths from current working directory before we change it
+ const sourcePathArg = path.resolve(args[0]);
+ const outputPathArg = args[1] ? path.resolve(args[1]) : null;
+
+ try {
+ const mergedCount = await includeMarkdownInDirectory(sourcePathArg, outputPathArg);
+ console.log(`Processed ${mergedCount} markdown file(s)`);
+ } catch (err) {
+ console.error('Error merging markdown files:', err);
+ process.exit(1);
+ }
+}
+
+const isMain = process.argv[1] === fileURLToPath(import.meta.url);
+
+if (isMain) {
+ main(process.argv);
+}
diff --git a/src/markdown-includer/includeMarkdown.test.js b/src/markdown-includer/includeMarkdown.test.js
new file mode 100644
index 0000000..8e3d0a2
--- /dev/null
+++ b/src/markdown-includer/includeMarkdown.test.js
@@ -0,0 +1,179 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { promises as fs } from 'fs';
+import os from 'os';
+import path from 'path';
+
+import { includeMarkdownFiles, includeMarkdownInDirectory } from './includeMarkdown.js';
+
+async function createTempDir() {
+ const prefix = path.join(os.tmpdir(), 'markdown-includer-');
+ return fs.mkdtemp(prefix);
+}
+
+async function writeFile(dir, name, content) {
+ const filePath = path.join(dir, name);
+ await fs.writeFile(filePath, content, 'utf8');
+ return filePath;
+}
+
+async function readFile(filePath) {
+ return fs.readFile(filePath, 'utf8');
+}
+
+test('mergeMarkdownFiles replaces include tag with referenced file content', async () => {
+ const dir = await createTempDir();
+
+ await writeFile(dir, 'a.md', '# Title A');
+ const output = path.join(dir, 'out.md');
+
+ // Template with inner value pointing at the included file (relative path)
+ await writeFile(dir, 'out.md', 'a.md');
+
+ const didMerge = await includeMarkdownFiles(output);
+ assert.equal(didMerge, true);
+
+ const result = await readFile(output);
+ assert.equal(result, '# Title A');
+});
+
+
+test('mergeMarkdownFiles does nothing when marker is missing', async () => {
+ const dir = await createTempDir();
+ const output = path.join(dir, 'out.md');
+
+ await writeFile(dir, 'out.md', 'No markers here');
+
+ const didMerge = await includeMarkdownFiles(output);
+ assert.equal(didMerge, false);
+
+ const result = await readFile(output);
+ assert.equal(result, 'No markers here');
+});
+
+
+test('mergeMarkdownFiles does nothing when closing tag is missing', async () => {
+ const dir = await createTempDir();
+ const output = path.join(dir, 'out.md');
+
+ await writeFile(dir, 'out.md', 'a.md');
+
+ const didMerge = await includeMarkdownFiles(output);
+ assert.equal(didMerge, false);
+
+ const result = await readFile(output);
+ assert.equal(result, 'a.md');
+});
+
+
+test('mergeMarkdownFiles replaces multiple include tags in order', async () => {
+ const dir = await createTempDir();
+
+ await writeFile(dir, 'one.md', '# One');
+ await writeFile(dir, 'two.md', '# Two');
+
+ const output = path.join(dir, 'out.md');
+ await writeFile(
+ dir,
+ 'out.md',
+ 'Header\n\none.md\n\nMiddle\n\ntwo.md\n\nFooter'
+ );
+
+ const didMerge = await includeMarkdownFiles(output);
+ assert.equal(didMerge, true);
+
+ const result = await readFile(output);
+ assert.equal(
+ result,
+ 'Header\n\n# One\n\nMiddle\n\n# Two\n\nFooter'
+ );
+});
+
+
+test('includeMarkdownInDirectory recursively processes markdown files in nested folders', async () => {
+ const dir = await createTempDir();
+ const nestedDir = path.join(dir, 'nested', 'deeper');
+ await fs.mkdir(nestedDir, { recursive: true });
+
+ await writeFile(dir, 'shared.md', '# Shared');
+ const rootOutput = await writeFile(dir, 'root.md', 'Top\nshared.md');
+ const nestedOutput = await writeFile(
+ nestedDir,
+ 'nested.md',
+ 'Inner\n../../shared.md'
+ );
+ const ignoredFile = await writeFile(
+ nestedDir,
+ 'notes.txt',
+ '../../shared.md'
+ );
+
+ const mergedCount = await includeMarkdownInDirectory(dir);
+ assert.equal(mergedCount, 2);
+
+ assert.equal(await readFile(rootOutput), 'Top\n# Shared');
+ assert.equal(await readFile(nestedOutput), 'Inner\n# Shared');
+ assert.equal(await readFile(ignoredFile), '../../shared.md');
+});
+
+
+test('mergeMarkdownFiles supports absolute include paths', async () => {
+ const dir = await createTempDir();
+ const includedFile = await writeFile(dir, 'absolute.md', '# Absolute Content');
+ const output = await writeFile(
+ dir,
+ 'absolute-out.md',
+ `Before\n${includedFile}\nAfter`
+ );
+
+ const didMerge = await includeMarkdownFiles(output);
+ assert.equal(didMerge, true);
+ assert.equal(await readFile(output), 'Before\n# Absolute Content\nAfter');
+});
+
+
+test('mergeMarkdownFiles expands a directory include into markdown file contents', async () => {
+ const dir = await createTempDir();
+ const docsDir = path.join(dir, 'docs');
+ await fs.mkdir(path.join(docsDir, 'nested'), { recursive: true });
+
+ await writeFile(docsDir, 'b.md', '# B');
+ await writeFile(docsDir, 'a.md', '# A');
+ await writeFile(docsDir, 'ignore.txt', 'ignored');
+ await writeFile(path.join(docsDir, 'nested'), 'c.md', '# C');
+
+ const output = await writeFile(
+ dir,
+ 'folder-out.md',
+ 'Before\ndocs\nAfter'
+ );
+
+ const didMerge = await includeMarkdownFiles(output);
+ assert.equal(didMerge, true);
+ assert.equal(await readFile(output), 'Before\n# A\n\n# B\n\n# C\nAfter');
+});
+
+
+test('includeMarkdownInDirectory processes a single markdown file path', async () => {
+ const dir = await createTempDir();
+ await writeFile(dir, 'single-source.md', '# Single Source');
+ const output = await writeFile(
+ dir,
+ 'single-target.md',
+ 'Start\nsingle-source.md'
+ );
+
+ const mergedCount = await includeMarkdownInDirectory(output);
+ assert.equal(mergedCount, 1);
+ assert.equal(await readFile(output), 'Start\n# Single Source');
+});
+
+
+test('includeMarkdownInDirectory ignores a non-markdown file path', async () => {
+ const dir = await createTempDir();
+ const textFile = await writeFile(dir, 'plain.txt', 'ignored.md');
+
+ const mergedCount = await includeMarkdownInDirectory(textFile);
+ assert.equal(mergedCount, 0);
+ assert.equal(await readFile(textFile), 'ignored.md');
+});
diff --git a/src/markdown-includer/package.json b/src/markdown-includer/package.json
new file mode 100644
index 0000000..911c560
--- /dev/null
+++ b/src/markdown-includer/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "markdown-includer",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "bin": {
+ "markdown-includer": "includeMarkdown.js"
+ },
+ "scripts": {
+ "merge": "node includeMarkdown.js",
+ "markdown-includer": "node includeMarkdown.js",
+ "test": "node --test"
+ }
+}
diff --git a/src/markdown-includer/readme.md b/src/markdown-includer/readme.md
new file mode 100644
index 0000000..77e3ce1
--- /dev/null
+++ b/src/markdown-includer/readme.md
@@ -0,0 +1,47 @@
+# Markdown Includer
+
+This utility replaces `...` tags in markdown files with the contents of the referenced markdown files. You can point the include tag at either a single markdown file or a folder. When the tag points at a folder, the tool reads all `.md` files under that folder recursively, sorts them by path, and inserts their contents separated by blank lines.
+
+## Run the script
+
+From this directory, pass a file or a folder path:
+
+```sh
+node includeMarkdown.js ./tests
+```
+
+Or with the npm script:
+
+```sh
+npm run merge -- ./tests
+```
+
+## Run the tests
+
+```sh
+npm test
+```
+
+## Packaging
+
+This tool is packaged as a small Node.js CLI module.
+
+- The package configuration lives in [src/markdown-includer/package.json](../../../src/markdown-includer/package.json)
+- It uses ESM via the `"type": "module"` setting
+- The CLI command is exposed through the `bin` entry as `markdown-includer`
+- The npm scripts provide shortcuts for running the merge command and the test suite
+
+## Example include tag
+
+```md
+mdwithtext1.md
+mdwithtext2.md
+```
+
+You can also include a folder:
+
+```md
+testing3/mdmultiple
+```
+
+That folder include expands all markdown files in the folder tree in path-sorted order.
diff --git a/src/resolve-hugo-links/adjust-hugo-links.js b/src/resolve-hugo-links/adjust-hugo-links.js
new file mode 100644
index 0000000..347312b
--- /dev/null
+++ b/src/resolve-hugo-links/adjust-hugo-links.js
@@ -0,0 +1,186 @@
+#!/usr/bin/env node
+
+/**
+ * adjust-hugo-links.js
+ *
+ * Transforms relative links in Markdown files so they resolve correctly
+ * when Hugo serves pages with trailing-slash URLs.
+ *
+ * Problem:
+ * Production serves pages WITHOUT trailing slashes (e.g. /library/overview/release-notes).
+ * Hugo serves pages WITH trailing slashes (e.g. /library/overview/release-notes/).
+ * Browsers resolve relative links from the "base directory" of the URL.
+ * A trailing slash means the last segment IS the directory, shifting resolution one level deeper.
+ * This causes every relative link to land one level too shallow on production
+ * (or one level too deep on Hugo, depending on perspective).
+ *
+ * Solution:
+ * Source Markdown (content/) has links authored for production depth.
+ * This script adds one "../" to every relative link in content-processed/
+ * so Hugo resolves them to the correct target.
+ *
+ * Usage:
+ * node script/adjust-hugo-links.js
+ */
+
+import { promises as fs } from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+
+/**
+ * Add one level of ../ to a relative URL.
+ * Returns the URL unchanged if it's absolute, external, or anchor-only.
+ */
+function adjustRelativeUrl(url) {
+ if (
+ !url ||
+ url.startsWith('/') ||
+ url.startsWith('#') ||
+ /^[a-z][a-z0-9+.-]*:/i.test(url)
+ ) {
+ return url;
+ }
+
+ // Separate path from fragment
+ const hashIndex = url.indexOf('#');
+ const pathPart = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
+ const fragment = hashIndex >= 0 ? url.slice(hashIndex) : '';
+
+ if (!pathPart) return url; // anchor-only
+
+ // ./foo → ../foo
+ if (pathPart.startsWith('./')) {
+ return '../' + pathPart.slice(2) + fragment;
+ }
+
+ // ../foo → ../../foo (and deeper)
+ // bare relative → ../bare
+ return '../' + pathPart + fragment;
+}
+
+/**
+ * Process a single line of Markdown, adjusting relative links.
+ * Handles both standard Markdown links and Hugo shortcode link= attributes.
+ */
+function adjustLinksInLine(line) {
+ // Skip lines that are entirely inline code or HTML comments
+ if (line.trimStart().startsWith('