From efd1e8736a2d3c764007683393954ab270fb5afc Mon Sep 17 00:00:00 2001
From: Patrick Kollitsch <davidsneighbourdev+gh@gmail.com>
Date: Fri, 17 Apr 2026 00:23:52 +0000
Subject: [PATCH] ci(fix): add all quick start steps to the test script

---
 scripts/test-hugo-quickstart.ts | 1171 +++++++++++++++++++++++++++++++++++++---------------------
 1 files changed, 747 insertions(+), 424 deletions(-)

diff --git a/scripts/test-hugo-quickstart.ts b/scripts/test-hugo-quickstart.ts
index 03ea8bc..5bedaae 100644
--- a/scripts/test-hugo-quickstart.ts
+++ b/scripts/test-hugo-quickstart.ts
@@ -1,69 +1,68 @@
 #!/usr/bin/env node
 
 import { spawn } from "node:child_process";
-import { mkdtemp, readFile, rm, access } from "node:fs/promises";
 import { constants } from "node:fs";
+import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
 import { tmpdir } from "node:os";
 import { join } from "node:path";
 
 interface CommandResult {
-    code: number | null;
-    signal: NodeJS.Signals | null;
-    stdout: string;
-    stderr: string;
-    combined: string;
-    durationMs: number;
+	code: number | null;
+	signal: NodeJS.Signals | null;
+	stdout: string;
+	stderr: string;
+	combined: string;
+	durationMs: number;
 }
 
 interface StepDefinition {
-    name: string;
-    command: string;
-    args: string[];
-    cwd: string;
-    expectedFiles?: string[];
+	name: string;
+	command: string;
+	args: string[];
+	cwd: string;
+	expectedFiles?: string[];
 }
 
 interface RoutineOptions {
-    projectName: string;
-    themeRepo: string;
-    themeDir: string;
-    themeName: string;
-    configFile: string;
-    keepOnSuccess: boolean;
-    keepOnFailure: boolean;
-    runContentSmokeTest: boolean;
-    verbose: boolean;
+	projectName: string;
+	themeRepo: string;
+	themeDir: string;
+	themeName: string;
+	configFile: string;
+	keepOnSuccess: boolean;
+	keepOnFailure: boolean;
+	verbose: boolean;
 }
 
 interface StepReport {
-    step: string;
-    commandLine: string;
-    cwd: string;
-    result: CommandResult;
+	step: string;
+	commandLine: string;
+	cwd: string;
+	result: CommandResult;
 }
 
 interface HtmlAssertion {
-    description: string;
-    test: (html: string) => boolean;
+	description: string;
+	test: (html: string) => boolean;
 }
 
 const DEFAULT_OPTIONS: RoutineOptions = {
-    projectName: "quickstart",
-    themeRepo: "https://github.com/theNewDynamic/gohugo-theme-ananke.git",
-    themeDir: "themes/ananke",
-    themeName: "ananke",
-    configFile: "hugo.toml",
-    keepOnSuccess: false,
-    keepOnFailure: true,
-    runContentSmokeTest: true,
-    verbose: true,
+	projectName: "quickstart",
+	themeRepo: "https://github.com/theNewDynamic/gohugo-theme-ananke.git",
+	themeDir: "themes/ananke",
+	themeName: "ananke",
+	configFile: "hugo.toml",
+	keepOnSuccess: false,
+	keepOnFailure: true,
+	verbose: true,
 };
 
 /**
  * Print CLI help.
  */
 function printHelp(): void {
-    console.log(`
+	console.log(
+		`
 Usage:
   node scripts/test-hugo-quickstart.ts [options]
 
@@ -75,15 +74,10 @@
   --config-file=<file>          Hugo config file to update
   --keep-on-success             Do not delete the temp directory when the test passes
   --no-keep-on-failure          Delete the temp directory when the test fails
-  --no-content-smoke-test       Skip "hugo new foo.md"
   --quiet                       Reduce step logging
   --help                        Show this help
-
-Examples:
-  node scripts/test-hugo-quickstart.ts
-  node scripts/test-hugo-quickstart.ts --keep-on-success
-  node scripts/test-hugo-quickstart.ts --project-name=quickstart-test
-`.trim());
+`.trim(),
+	);
 }
 
 /**
@@ -94,63 +88,58 @@
  * @throws Error when an unknown argument is passed.
  */
 function parseArgs(argv: string[]): RoutineOptions {
-    const options: RoutineOptions = { ...DEFAULT_OPTIONS };
+	const options: RoutineOptions = { ...DEFAULT_OPTIONS };
 
-    for (const arg of argv) {
-        if (arg === "--help") {
-            printHelp();
-            process.exit(0);
-        }
+	for (const arg of argv) {
+		if (arg === "--help") {
+			printHelp();
+			process.exit(0);
+		}
 
-        if (arg === "--keep-on-success") {
-            options.keepOnSuccess = true;
-            continue;
-        }
+		if (arg === "--keep-on-success") {
+			options.keepOnSuccess = true;
+			continue;
+		}
 
-        if (arg === "--no-keep-on-failure") {
-            options.keepOnFailure = false;
-            continue;
-        }
+		if (arg === "--no-keep-on-failure") {
+			options.keepOnFailure = false;
+			continue;
+		}
 
-        if (arg === "--no-content-smoke-test") {
-            options.runContentSmokeTest = false;
-            continue;
-        }
+		if (arg === "--quiet") {
+			options.verbose = false;
+			continue;
+		}
 
-        if (arg === "--quiet") {
-            options.verbose = false;
-            continue;
-        }
+		if (arg.startsWith("--project-name=")) {
+			options.projectName = arg.slice("--project-name=".length);
+			continue;
+		}
 
-        if (arg.startsWith("--project-name=")) {
-            options.projectName = arg.slice("--project-name=".length);
-            continue;
-        }
+		if (arg.startsWith("--theme-repo=")) {
+			options.themeRepo = arg.slice("--theme-repo=".length);
+			continue;
+		}
 
-        if (arg.startsWith("--theme-repo=")) {
-            options.themeRepo = arg.slice("--theme-repo=".length);
-            continue;
-        }
+		if (arg.startsWith("--theme-dir=")) {
+			options.themeDir = arg.slice("--theme-dir=".length);
+			continue;
+		}
 
-        if (arg.startsWith("--theme-dir=")) {
-            options.themeDir = arg.slice("--theme-dir=".length);
-            continue;
-        }
+		if (arg.startsWith("--theme-name=")) {
+			options.themeName = arg.slice("--theme-name=".length);
+			continue;
+		}
 
-        if (arg.startsWith("--theme-name=")) {
-            options.themeName = arg.slice("--theme-name=".length);
-            continue;
-        }
+		if (arg.startsWith("--config-file=")) {
+			options.configFile = arg.slice("--config-file=".length);
+			continue;
+		}
 
-        if (arg.startsWith("--config-file=")) {
-            options.configFile = arg.slice("--config-file=".length);
-            continue;
-        }
+		throw new Error(`Unknown argument: ${arg}`);
+	}
 
-        throw new Error(`Unknown argument: ${arg}`);
-    }
-
-    return options;
+	return options;
 }
 
 /**
@@ -161,9 +150,9 @@
  * @returns Full command line.
  */
 function formatCommand(command: string, args: string[]): string {
-    return [command, ...args]
-        .map((part) => (/\s/.test(part) ? JSON.stringify(part) : part))
-        .join(" ");
+	return [command, ...args]
+		.map((part) => (/\s/.test(part) ? JSON.stringify(part) : part))
+		.join(" ");
 }
 
 /**
@@ -175,119 +164,209 @@
  * @returns Command execution result.
  */
 async function runCommand(
-    command: string,
-    args: string[],
-    cwd: string,
+	command: string,
+	args: string[],
+	cwd: string,
 ): Promise<CommandResult> {
-    const started = Date.now();
+	const started = Date.now();
 
-    return new Promise<CommandResult>((resolve, reject) => {
-        const child = spawn(command, args, {
-            cwd,
-            env: process.env,
-            stdio: ["ignore", "pipe", "pipe"],
-        });
+	return new Promise<CommandResult>((resolve, reject) => {
+		const child = spawn(command, args, {
+			cwd,
+			env: process.env,
+			stdio: ["ignore", "pipe", "pipe"],
+		});
 
-        let stdout = "";
-        let stderr = "";
+		let stdout = "";
+		let stderr = "";
 
-        child.stdout.on("data", (chunk: Buffer | string) => {
-            stdout += chunk.toString();
-        });
+		child.stdout.on("data", (chunk: Buffer | string) => {
+			stdout += chunk.toString();
+		});
 
-        child.stderr.on("data", (chunk: Buffer | string) => {
-            stderr += chunk.toString();
-        });
+		child.stderr.on("data", (chunk: Buffer | string) => {
+			stderr += chunk.toString();
+		});
 
-        child.on("error", (error: Error) => {
-            reject(error);
-        });
+		child.on("error", (error: Error) => {
+			reject(error);
+		});
 
-        child.on("close", (code, signal) => {
-            const durationMs = Date.now() - started;
-            const combined = [stdout, stderr].filter(Boolean).join(stdout && stderr ? "\n" : "");
+		child.on("close", (code, signal) => {
+			const durationMs = Date.now() - started;
+			const combined = [stdout, stderr]
+				.filter(Boolean)
+				.join(stdout && stderr ? "\n" : "");
 
-            resolve({
-                code,
-                signal,
-                stdout,
-                stderr,
-                combined,
-                durationMs,
-            });
-        });
-    });
+			resolve({
+				code,
+				signal,
+				stdout,
+				stderr,
+				combined,
+				durationMs,
+			});
+		});
+	});
 }
 
 /**
- * Ensure a file exists.
+ * Ensure a file or directory exists.
  *
- * @param filePath Absolute file path.
+ * @param filePath Absolute path to check.
  */
 async function assertFileExists(filePath: string): Promise<void> {
-    await access(filePath, constants.F_OK);
+	await access(filePath, constants.F_OK);
 }
 
 /**
- * Execute one quick-start step and validate success.
+ * Ensure a file or directory does not exist.
+ *
+ * @param filePath Absolute path to check.
+ */
+async function assertFileDoesNotExist(filePath: string): Promise<void> {
+	try {
+		await access(filePath, constants.F_OK);
+		throw new Error(`Unexpected path exists: ${filePath}`);
+	} catch (error: unknown) {
+		if (
+			error instanceof Error &&
+			error.message.startsWith("Unexpected path exists:")
+		) {
+			throw error;
+		}
+	}
+}
+
+/**
+ * Read a UTF-8 text file.
+ *
+ * @param filePath Absolute file path.
+ * @returns File contents.
+ */
+async function readTextFile(filePath: string): Promise<string> {
+	return readFile(filePath, "utf8");
+}
+
+/**
+ * Write a UTF-8 text file.
+ *
+ * @param filePath Absolute file path.
+ * @param content File contents.
+ */
+async function writeTextFile(filePath: string, content: string): Promise<void> {
+	await writeFile(filePath, content, "utf8");
+}
+
+/**
+ * Remove the generated public directory inside the temporary project.
+ *
+ * @param projectRoot Absolute path to the temporary quickstart project.
+ */
+async function removePublicDir(projectRoot: string): Promise<void> {
+	const publicPath = join(projectRoot, "public");
+	await rm(publicPath, { recursive: true, force: true });
+}
+
+/**
+ * Execute one step and validate success.
  *
  * @param step Step definition.
  * @returns Step report.
  * @throws Error when the command fails or an expected file is missing.
  */
 async function executeStep(step: StepDefinition): Promise<StepReport> {
-    const result = await runCommand(step.command, step.args, step.cwd);
-    const commandLine = formatCommand(step.command, step.args);
+	const result = await runCommand(step.command, step.args, step.cwd);
+	const commandLine = formatCommand(step.command, step.args);
 
-    if (result.code !== 0) {
-        const details = [
-            `Step failed: ${step.name}`,
-            `Command: ${commandLine}`,
-            `Working directory: ${step.cwd}`,
-            `Exit code: ${String(result.code)}`,
-            result.signal ? `Signal: ${result.signal}` : "",
-            result.stdout ? `STDOUT:\n${result.stdout}` : "",
-            result.stderr ? `STDERR:\n${result.stderr}` : "",
-        ]
-            .filter(Boolean)
-            .join("\n\n");
+	if (result.code !== 0) {
+		const details = [
+			`Step failed: ${step.name}`,
+			`Command: ${commandLine}`,
+			`Working directory: ${step.cwd}`,
+			`Exit code: ${String(result.code)}`,
+			result.signal ? `Signal: ${result.signal}` : "",
+			result.stdout ? `STDOUT:\n${result.stdout}` : "",
+			result.stderr ? `STDERR:\n${result.stderr}` : "",
+		]
+			.filter(Boolean)
+			.join("\n\n");
 
-        throw new Error(details);
-    }
+		throw new Error(details);
+	}
 
-    if (step.expectedFiles) {
-        for (const relativePath of step.expectedFiles) {
-            const absolutePath = join(step.cwd, relativePath);
+	if (step.expectedFiles) {
+		for (const relativePath of step.expectedFiles) {
+			const absolutePath = join(step.cwd, relativePath);
 
-            try {
-                await assertFileExists(absolutePath);
-            } catch (error: unknown) {
-                const message =
-                    error instanceof Error ? error.message : "Unknown file assertion error";
+			try {
+				await assertFileExists(absolutePath);
+			} catch (error: unknown) {
+				const message =
+					error instanceof Error
+						? error.message
+						: "Unknown file assertion error";
 
-                throw new Error(
-                    [
-                        `Step failed: ${step.name}`,
-                        `Command: ${commandLine}`,
-                        `Working directory: ${step.cwd}`,
-                        `Expected file missing: ${absolutePath}`,
-                        `Details: ${message}`,
-                        result.stdout ? `STDOUT:\n${result.stdout}` : "",
-                        result.stderr ? `STDERR:\n${result.stderr}` : "",
-                    ]
-                        .filter(Boolean)
-                        .join("\n\n"),
-                );
-            }
-        }
-    }
+				throw new Error(
+					[
+						`Step failed: ${step.name}`,
+						`Command: ${commandLine}`,
+						`Working directory: ${step.cwd}`,
+						`Expected file missing: ${absolutePath}`,
+						`Details: ${message}`,
+						result.stdout ? `STDOUT:\n${result.stdout}` : "",
+						result.stderr ? `STDERR:\n${result.stderr}` : "",
+					]
+						.filter(Boolean)
+						.join("\n\n"),
+				);
+			}
+		}
+	}
 
-    return {
-        step: step.name,
-        commandLine,
-        cwd: step.cwd,
-        result,
-    };
+	return {
+		step: step.name,
+		commandLine,
+		cwd: step.cwd,
+		result,
+	};
+}
+
+/**
+ * Determine whether a Hugo command generates output in `public/`.
+ *
+ * @param step Step definition.
+ * @returns True when the command is a build command.
+ */
+function isHugoBuildCommand(step: StepDefinition): boolean {
+	if (step.command !== "hugo") {
+		return false;
+	}
+
+	if (step.args.length === 0) {
+		return true;
+	}
+
+	if (step.args.includes("--buildDrafts")) {
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * Execute a Hugo build command after clearing the generated public directory.
+ *
+ * @param step Step definition.
+ * @param projectRoot Absolute path to the temporary quickstart project.
+ * @returns Step report.
+ */
+async function executeHugoBuildStep(
+	step: StepDefinition,
+	projectRoot: string,
+): Promise<StepReport> {
+	await removePublicDir(projectRoot);
+	return executeStep(step);
 }
 
 /**
@@ -297,321 +376,565 @@
  * @returns Escaped string.
  */
 function escapeRegExp(value: string): string {
-    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+	return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 }
 
 /**
- * Read a text file as UTF-8.
+ * Assert that Hugo config contains the expected theme assignment somewhere in
+ * the file, without requiring the whole config to match a fixed template.
  *
- * @param filePath Absolute file path.
- * @returns File contents.
- */
-async function readTextFile(filePath: string): Promise<string> {
-    return readFile(filePath, "utf8");
-}
-
-/**
- * Assert that Hugo config contains the expected theme assignment.
+ * Accepts either single or double quotes, for example:
+ * - theme = 'ananke'
+ * - theme = "ananke"
  *
  * @param configPath Absolute config path.
  * @param themeName Expected theme name.
- * @throws Error when the config does not contain the expected theme.
+ * @throws Error when the config does not contain the expected theme line.
  */
 async function assertThemeConfigured(
-    configPath: string,
-    themeName: string,
+	configPath: string,
+	themeName: string,
 ): Promise<void> {
-    const config = await readTextFile(configPath);
-    const themePattern = new RegExp(
-        String.raw`^\s*theme\s*=\s*['"]${escapeRegExp(themeName)}['"]\s*$`,
-        "m",
-    );
+	const config = await readTextFile(configPath);
+	const themePattern = new RegExp(
+		String.raw`^\s*theme\s*=\s*['"]${escapeRegExp(themeName)}['"]\s*$`,
+		"m",
+	);
 
-    if (!themePattern.test(config)) {
-        throw new Error(
-            [
-                "Strict assertion failed: theme configuration missing or incorrect.",
-                `Config file: ${configPath}`,
-                `Expected: theme = '${themeName}'`,
-                "Actual file contents:",
-                config,
-            ].join("\n\n"),
-        );
-    }
+	if (!themePattern.test(config)) {
+		throw new Error(
+			[
+				"Strict assertion failed: theme configuration missing or incorrect.",
+				`Config file: ${configPath}`,
+				`Expected to find a line like: theme = '${themeName}'`,
+				"Actual file contents:",
+				config,
+			].join("\n\n"),
+		);
+	}
 }
 
 /**
- * Return the strict homepage assertions.
- *
- * These are intentionally a mix of generic HTML checks and theme-oriented checks.
- * The goal is not to pin every byte of output, but to prove that the theme actually
- * rendered a plausible Ananke homepage.
+ * Return homepage assertions for the initial static build.
  *
  * @returns List of homepage assertions.
  */
 function getHomepageAssertions(): HtmlAssertion[] {
-    return [
-        {
-            description: "homepage contains an HTML document root",
-            test: (html: string): boolean => /<html\b/i.test(html),
-        },
-        {
-            description: "homepage contains a document title",
-            test: (html: string): boolean => /<title>[\s\S]*?<\/title>/i.test(html),
-        },
-        {
-            description: "homepage contains a body element",
-            test: (html: string): boolean => /<body\b/i.test(html),
-        },
-        {
-            description: "homepage contains at least one stylesheet reference",
-            test: (html: string): boolean =>
-                /<link\b[^>]*rel=["']stylesheet["'][^>]*>/i.test(html),
-        },
-        {
-            description: "homepage contains at least one navigation-related landmark",
-            test: (html: string): boolean =>
-                /<(nav|header)\b/i.test(html),
-        },
-        {
-            description: "homepage contains theme-generated CSS class markers",
-            test: (html: string): boolean =>
-                /\b(ma[0-9]|pa[0-9]|bg-black|near-white|sans-serif)\b/i.test(html),
-        },
-        {
-            description: "homepage contains a main content area",
-            test: (html: string): boolean =>
-                /<(main|article|section)\b/i.test(html),
-        },
-    ];
+	return [
+		{
+			description: "homepage contains an HTML document root",
+			test: (html: string): boolean => /<html\b/i.test(html),
+		},
+		{
+			description: "homepage contains a document title",
+			test: (html: string): boolean => /<title>[\s\S]*?<\/title>/i.test(html),
+		},
+		{
+			description: "homepage contains a body element",
+			test: (html: string): boolean => /<body\b/i.test(html),
+		},
+		{
+			description: "homepage contains at least one stylesheet reference",
+			test: (html: string): boolean =>
+				/<link\b[^>]*rel=["']stylesheet["'][^>]*>/i.test(html),
+		},
+		{
+			description: "homepage contains at least one navigation-related landmark",
+			test: (html: string): boolean => /<(nav|header)\b/i.test(html),
+		},
+		{
+			description: "homepage contains theme-generated CSS class markers",
+			test: (html: string): boolean =>
+				/\b(ma[0-9]|pa[0-9]|bg-black|near-white|sans-serif)\b/i.test(html),
+		},
+		{
+			description: "homepage contains a main content area",
+			test: (html: string): boolean => /<(main|article|section)\b/i.test(html),
+		},
+	];
 }
 
 /**
- * Assert that the generated homepage looks like a real themed render and not just
- * an empty or broken output file.
+ * Assert that the generated homepage looks like a real themed render.
  *
  * @param homepagePath Absolute path to `public/index.html`.
- * @throws Error when one or more strict assertions fail.
+ * @throws Error when one or more assertions fail.
  */
 async function assertHomepageLooksValid(homepagePath: string): Promise<void> {
-    const html = await readTextFile(homepagePath);
-    const failures: string[] = [];
+	const html = await readTextFile(homepagePath);
+	const failures: string[] = [];
 
-    if (html.trim().length === 0) {
-        throw new Error(
-            [
-                "Strict assertion failed: generated homepage is empty.",
-                `Homepage file: ${homepagePath}`,
-            ].join("\n\n"),
-        );
-    }
+	if (html.trim().length === 0) {
+		throw new Error(
+			[
+				"Strict assertion failed: generated homepage is empty.",
+				`Homepage file: ${homepagePath}`,
+			].join("\n\n"),
+		);
+	}
 
-    for (const assertion of getHomepageAssertions()) {
-        if (!assertion.test(html)) {
-            failures.push(`- ${assertion.description}`);
-        }
-    }
+	for (const assertion of getHomepageAssertions()) {
+		if (!assertion.test(html)) {
+			failures.push(`- ${assertion.description}`);
+		}
+	}
 
-    if (failures.length > 0) {
-        throw new Error(
-            [
-                "Strict assertion failed: generated homepage did not match expected render checks.",
-                `Homepage file: ${homepagePath}`,
-                "Failed assertions:",
-                ...failures,
-            ].join("\n"),
-        );
-    }
+	if (failures.length > 0) {
+		throw new Error(
+			[
+				"Strict assertion failed: generated homepage did not match expected render checks.",
+				`Homepage file: ${homepagePath}`,
+				"Failed assertions:",
+				...failures,
+			].join("\n"),
+		);
+	}
 }
 
 /**
- * Assert that generated sample content contains frontmatter and basic metadata.
+ * Extract the auto-generated date line from a Hugo content file with TOML frontmatter.
  *
- * @param contentPath Absolute path to the created content file.
- * @throws Error when the content file does not look like a valid Hugo content file.
+ * @param content Raw file contents.
+ * @returns Original date line.
+ * @throws Error when the date line is missing.
  */
-async function assertGeneratedContentLooksValid(contentPath: string): Promise<void> {
-    const content = await readTextFile(contentPath);
-    const failures: string[] = [];
+function extractGeneratedDateLine(content: string): string {
+	const match = content.match(/^\s*date\s*=\s*.+$/m);
 
-    if (!/^---\s*$/m.test(content) && !/^\+\+\+\s*$/m.test(content)) {
-        failures.push("- generated content does not appear to contain frontmatter delimiters");
-    }
+	if (!match) {
+		throw new Error(
+			[
+				"Strict assertion failed: could not find auto-generated date line in content file.",
+				"Actual file contents:",
+				content,
+			].join("\n\n"),
+		);
+	}
 
-    if (!/^\s*title\s*:/m.test(content) && !/^\s*title\s*=/m.test(content)) {
-        failures.push("- generated content does not contain a title field");
-    }
-
-    if (!/^\s*date\s*:/m.test(content) && !/^\s*date\s*=/m.test(content)) {
-        failures.push("- generated content does not contain a date field");
-    }
-
-    if (failures.length > 0) {
-        throw new Error(
-            [
-                "Strict assertion failed: generated content file did not match expected Hugo structure.",
-                `Content file: ${contentPath}`,
-                "Failed assertions:",
-                ...failures,
-                "",
-                "Actual file contents:",
-                content,
-            ].join("\n"),
-        );
-    }
+	return match[0];
 }
 
 /**
- * Run the Hugo quick-start verification routine.
+ * Replace the generated content with the requested sample draft while preserving
+ * the original date line created by `hugo new`.
+ *
+ * @param contentPath Absolute path to the content file.
+ */
+async function replaceGeneratedContent(contentPath: string): Promise<void> {
+	const original = await readTextFile(contentPath);
+	const dateLine = extractGeneratedDateLine(original);
+
+	const updated = [
+		"+++",
+		"title = 'My First Post'",
+		dateLine,
+		"draft = true",
+		"+++",
+		"## Introduction",
+		"",
+		"This is **bold** text, and this is *emphasized* text.",
+		"",
+		"Visit the [Hugo](https://gohugo.io) website!",
+		"",
+	].join("\n");
+
+	await writeTextFile(contentPath, updated);
+}
+
+/**
+ * Replace the root Hugo config with the requested quickstart config.
+ *
+ * @param configPath Absolute path to `hugo.toml`.
+ * @param themeName Theme name to set.
+ */
+async function replaceHugoConfig(
+	configPath: string,
+	themeName: string,
+): Promise<void> {
+	const content = [
+		"baseURL = 'https://example.com/'",
+		"locale = 'en-gb'",
+		"title = 'Ananke Test Quickstart'",
+		`theme = '${themeName}'`,
+		"",
+	].join("\n");
+
+	await writeTextFile(configPath, content);
+}
+
+/**
+ * Assert that the generated page contains the expected rendered draft content.
+ *
+ * @param pageHtml HTML from `public/foo/index.html`.
+ */
+function assertDraftPageRendered(pageHtml: string): void {
+	const failures: string[] = [];
+
+	if (!/<h2[^>]*>\s*Introduction\s*<\/h2>/i.test(pageHtml)) {
+		failures.push("- heading 'Introduction' was not rendered as an h2 element");
+	}
+
+	if (!/<strong>\s*bold\s*<\/strong>/i.test(pageHtml)) {
+		failures.push("- bold Markdown was not rendered as a <strong> element");
+	}
+
+	if (!/<em>\s*emphasized\s*<\/em>/i.test(pageHtml)) {
+		failures.push("- emphasized Markdown was not rendered as an <em> element");
+	}
+
+	if (
+		!/<a[^>]+href=["']https:\/\/gohugo\.io["'][^>]*>\s*Hugo\s*<\/a>/i.test(
+			pageHtml,
+		)
+	) {
+		failures.push("- Markdown link was not rendered as an anchor element");
+	}
+
+	if (!/My First Post/i.test(pageHtml)) {
+		failures.push("- post title was not visible on the rendered page");
+	}
+
+	if (failures.length > 0) {
+		throw new Error(
+			[
+				"Strict assertion failed: draft page content was not rendered as expected.",
+				"Failed assertions:",
+				...failures,
+			].join("\n"),
+		);
+	}
+}
+
+/**
+ * Assert that the generated homepage reflects updated title and locale configuration.
+ *
+ * Locale is checked strictly on the `<html>` tag.
+ *
+ * @param homepageHtml HTML from `public/index.html`.
+ */
+function assertUpdatedConfigInOutput(homepageHtml: string): void {
+	const failures: string[] = [];
+
+	if (!/Ananke Test Quickstart/i.test(homepageHtml)) {
+		failures.push(
+			"- updated site title was not visible in the generated output",
+		);
+	}
+
+	if (!/<html[^>]+lang=["']en-gb["'][^>]*>/i.test(homepageHtml)) {
+		failures.push(
+			"- updated locale 'en-gb' was not present in the <html lang=\"en-gb\"> tag",
+		);
+	}
+
+	if (failures.length > 0) {
+		throw new Error(
+			[
+				"Strict assertion failed: updated config was not reflected in the generated output.",
+				"Failed assertions:",
+				...failures,
+			].join("\n"),
+		);
+	}
+}
+
+/**
+ * Assert that the draft page is not part of the production build.
+ *
+ * @param projectRoot Project root.
+ * @param homepagePath Absolute path to `public/index.html`.
+ */
+async function assertDraftHiddenInProduction(
+	projectRoot: string,
+	homepagePath: string,
+): Promise<void> {
+	const draftOutputPath = join(projectRoot, "public", "foo", "index.html");
+	await assertFileDoesNotExist(draftOutputPath);
+
+	const homepageHtml = await readTextFile(homepagePath);
+
+	if (/My First Post/i.test(homepageHtml)) {
+		throw new Error(
+			[
+				"Strict assertion failed: draft post title was visible in the production homepage output.",
+				`Homepage file: ${homepagePath}`,
+			].join("\n\n"),
+		);
+	}
+}
+
+/**
+ * Run the full Hugo quickstart verification routine.
  *
  * @param options Runtime options.
  * @returns Process exit code.
  */
 async function runRoutine(options: RoutineOptions): Promise<number> {
-    const sandboxRoot = await mkdtemp(join(tmpdir(), "hugo-quickstart-"));
-    const projectRoot = join(sandboxRoot, options.projectName);
+	const sandboxRoot = await mkdtemp(join(tmpdir(), "hugo-quickstart-"));
+	const projectRoot = join(sandboxRoot, options.projectName);
 
-    const reports: StepReport[] = [];
+	const reports: StepReport[] = [];
 
-    const steps: StepDefinition[] = [
-        {
-            name: "Create Hugo project",
-            command: "hugo",
-            args: ["new", "project", options.projectName],
-            cwd: sandboxRoot,
-            expectedFiles: [join(options.projectName, options.configFile)],
-        },
-        {
-            name: "Initialise Git repository",
-            command: "git",
-            args: ["init"],
-            cwd: projectRoot,
-            expectedFiles: [".git"],
-        },
-        {
-            name: "Add theme as Git submodule",
-            command: "git",
-            args: ["submodule", "add", options.themeRepo, options.themeDir],
-            cwd: projectRoot,
-            expectedFiles: [options.themeDir, ".gitmodules"],
-        },
-        {
-            name: "Configure theme in Hugo config",
-            command: "bash",
-            args: ["-lc", `printf "\\ntheme = '${options.themeName}'\\n" >> ${JSON.stringify(options.configFile)}`],
-            cwd: projectRoot,
-            expectedFiles: [options.configFile],
-        },
-        {
-            name: "Build site",
-            command: "hugo",
-            args: [],
-            cwd: projectRoot,
-            expectedFiles: ["public/index.html"],
-        },
-    ];
+	const steps: StepDefinition[] = [
+		{
+			name: "Create Hugo project",
+			command: "hugo",
+			args: ["new", "project", options.projectName],
+			cwd: sandboxRoot,
+			expectedFiles: [join(options.projectName, options.configFile)],
+		},
+		{
+			name: "Initialise Git repository",
+			command: "git",
+			args: ["init"],
+			cwd: projectRoot,
+			expectedFiles: [".git"],
+		},
+		{
+			name: "Add theme as Git submodule",
+			command: "git",
+			args: ["submodule", "add", options.themeRepo, options.themeDir],
+			cwd: projectRoot,
+			expectedFiles: [options.themeDir, ".gitmodules"],
+		},
+		{
+			name: "Configure theme in Hugo config",
+			command: "bash",
+			args: [
+				"-lc",
+				`printf "\\ntheme = '${options.themeName}'\\n" >> ${JSON.stringify(options.configFile)}`,
+			],
+			cwd: projectRoot,
+			expectedFiles: [options.configFile],
+		},
+		{
+			name: "Build site",
+			command: "hugo",
+			args: [],
+			cwd: projectRoot,
+			expectedFiles: ["public/index.html"],
+		},
+	];
 
-    if (options.runContentSmokeTest) {
-        steps.push({
-            name: "Create sample content",
-            command: "hugo",
-            args: ["new", "foo.md"],
-            cwd: projectRoot,
-            expectedFiles: ["content/foo.md"],
-        });
-    }
+	try {
+		console.log(`Test root: ${sandboxRoot}`);
+		console.log(`Project root: ${projectRoot}`);
 
-    try {
-        console.log(`Test root: ${sandboxRoot}`);
-        console.log(`Project root: ${projectRoot}`);
+		for (const step of steps) {
+			if (options.verbose) {
+				console.log(`\n[RUN] ${step.name}`);
+				console.log(`      ${formatCommand(step.command, step.args)}`);
+			}
 
-        for (const step of steps) {
-            if (options.verbose) {
-                console.log(`\n[RUN] ${step.name}`);
-                console.log(`      ${formatCommand(step.command, step.args)}`);
-            }
+			const report = isHugoBuildCommand(step)
+				? await executeHugoBuildStep(step, projectRoot)
+				: await executeStep(step);
 
-            const report = await executeStep(step);
-            reports.push(report);
+			reports.push(report);
 
-            if (options.verbose) {
-                console.log(
-                    `[OK ] ${step.name} (${report.result.durationMs} ms, exit ${String(report.result.code)})`,
-                );
+			if (options.verbose) {
+				console.log(
+					`[OK ] ${step.name} (${report.result.durationMs} ms, exit ${String(report.result.code)})`,
+				);
 
-                const trimmedOutput = report.result.combined.trim();
-                if (trimmedOutput) {
-                    console.log(trimmedOutput);
-                }
-            }
-        }
+				const trimmedOutput = report.result.combined.trim();
+				if (trimmedOutput) {
+					console.log(trimmedOutput);
+				}
+			}
+		}
 
-        const configPath = join(projectRoot, options.configFile);
-        const homepagePath = join(projectRoot, "public/index.html");
+		const configPath = join(projectRoot, options.configFile);
+		const homepagePath = join(projectRoot, "public/index.html");
+		const contentPath = join(projectRoot, "content/foo.md");
+		const draftOutputPath = join(projectRoot, "public", "foo", "index.html");
 
-        console.log("\n[RUN] Strict config assertion");
-        await assertThemeConfigured(configPath, options.themeName);
-        console.log("[OK ] Strict config assertion");
+		console.log("\n[RUN] Strict config assertion");
+		await assertThemeConfigured(configPath, options.themeName);
+		console.log("[OK ] Strict config assertion");
 
-        console.log("\n[RUN] Strict homepage assertion");
-        await assertHomepageLooksValid(homepagePath);
-        console.log("[OK ] Strict homepage assertion");
+		console.log("\n[RUN] Strict homepage assertion");
+		await assertHomepageLooksValid(homepagePath);
+		console.log("[OK ] Strict homepage assertion");
 
-        if (options.runContentSmokeTest) {
-            const contentPath = join(projectRoot, "content/foo.md");
+		console.log("\n[RUN] Create sample content");
+		const createContentStep: StepDefinition = {
+			name: "Create sample content",
+			command: "hugo",
+			args: ["new", "foo.md"],
+			cwd: projectRoot,
+			expectedFiles: ["content/foo.md"],
+		};
+		const createContentReport = await executeStep(createContentStep);
+		reports.push(createContentReport);
 
-            console.log("\n[RUN] Strict generated content assertion");
-            await assertGeneratedContentLooksValid(contentPath);
-            console.log("[OK ] Strict generated content assertion");
-        }
+		if (options.verbose) {
+			console.log(
+				`      ${formatCommand(createContentStep.command, createContentStep.args)}`,
+			);
+			console.log(
+				`[OK ] ${createContentStep.name} (${createContentReport.result.durationMs} ms, exit ${String(createContentReport.result.code)})`,
+			);
 
-        console.log("\nResult: PASS");
+			const trimmedOutput = createContentReport.result.combined.trim();
+			if (trimmedOutput) {
+				console.log(trimmedOutput);
+			}
+		}
 
-        if (options.keepOnSuccess) {
-            console.log(`Keeping successful test directory: ${projectRoot}`);
-        } else {
-            await rm(sandboxRoot, { recursive: true, force: true });
-            console.log(`Deleted successful test directory: ${sandboxRoot}`);
-        }
+		console.log("\n[RUN] Replace generated content with quickstart sample");
+		await replaceGeneratedContent(contentPath);
+		console.log("[OK ] Replace generated content with quickstart sample");
 
-        return 0;
-    } catch (error: unknown) {
-        const message = error instanceof Error ? error.message : "Unknown error";
+		console.log("\n[RUN] Build drafts and verify rendered draft content");
+		const draftBuildStep: StepDefinition = {
+			name: "Build site with drafts",
+			command: "hugo",
+			args: ["--buildDrafts"],
+			cwd: projectRoot,
+			expectedFiles: ["public/index.html", "public/foo/index.html"],
+		};
+		const draftBuildReport = await executeHugoBuildStep(
+			draftBuildStep,
+			projectRoot,
+		);
+		reports.push(draftBuildReport);
 
-        console.error("\nResult: FAIL");
-        console.error(message);
+		if (options.verbose) {
+			console.log(
+				`      ${formatCommand(draftBuildStep.command, draftBuildStep.args)}`,
+			);
+			console.log(
+				`[OK ] ${draftBuildStep.name} (${draftBuildReport.result.durationMs} ms, exit ${String(draftBuildReport.result.code)})`,
+			);
 
-        if (reports.length > 0) {
-            console.error("\nCompleted command steps before failure:");
-            for (const report of reports) {
-                console.error(`- ${report.step}`);
-            }
-        }
+			const trimmedOutput = draftBuildReport.result.combined.trim();
+			if (trimmedOutput) {
+				console.log(trimmedOutput);
+			}
+		}
 
-        if (options.keepOnFailure) {
-            console.error(`\nKept failing test directory for inspection: ${projectRoot}`);
-        } else {
-            await rm(sandboxRoot, { recursive: true, force: true });
-            console.error(`\nDeleted failing test directory: ${sandboxRoot}`);
-        }
+		const draftPageHtml = await readTextFile(draftOutputPath);
+		assertDraftPageRendered(draftPageHtml);
+		console.log("[OK ] Build drafts and verify rendered draft content");
 
-        return 1;
-    }
+		console.log("\n[RUN] Replace root hugo.toml with quickstart config");
+		await replaceHugoConfig(configPath, options.themeName);
+		console.log("[OK ] Replace root hugo.toml with quickstart config");
+
+		console.log("\n[RUN] Build drafts and verify updated title and locale");
+		const configBuildStep: StepDefinition = {
+			name: "Build site with updated config and drafts",
+			command: "hugo",
+			args: ["--buildDrafts"],
+			cwd: projectRoot,
+			expectedFiles: ["public/index.html", "public/foo/index.html"],
+		};
+		const configBuildReport = await executeHugoBuildStep(
+			configBuildStep,
+			projectRoot,
+		);
+		reports.push(configBuildReport);
+
+		if (options.verbose) {
+			console.log(
+				`      ${formatCommand(configBuildStep.command, configBuildStep.args)}`,
+			);
+			console.log(
+				`[OK ] ${configBuildStep.name} (${configBuildReport.result.durationMs} ms, exit ${String(configBuildReport.result.code)})`,
+			);
+
+			const trimmedOutput = configBuildReport.result.combined.trim();
+			if (trimmedOutput) {
+				console.log(trimmedOutput);
+			}
+		}
+
+		const updatedHomepageHtml = await readTextFile(homepagePath);
+		assertUpdatedConfigInOutput(updatedHomepageHtml);
+		console.log("[OK ] Build drafts and verify updated title and locale");
+
+		console.log("\n[RUN] Production build should exclude draft content");
+		const productionBuildStep: StepDefinition = {
+			name: "Build production site without drafts",
+			command: "hugo",
+			args: [],
+			cwd: projectRoot,
+			expectedFiles: ["public/index.html"],
+		};
+		const productionBuildReport = await executeHugoBuildStep(
+			productionBuildStep,
+			projectRoot,
+		);
+		reports.push(productionBuildReport);
+
+		if (options.verbose) {
+			console.log(
+				`      ${formatCommand(productionBuildStep.command, productionBuildStep.args)}`,
+			);
+			console.log(
+				`[OK ] ${productionBuildStep.name} (${productionBuildReport.result.durationMs} ms, exit ${String(productionBuildReport.result.code)})`,
+			);
+
+			const trimmedOutput = productionBuildReport.result.combined.trim();
+			if (trimmedOutput) {
+				console.log(trimmedOutput);
+			}
+		}
+
+		await assertDraftHiddenInProduction(projectRoot, homepagePath);
+		console.log("[OK ] Production build should exclude draft content");
+
+		console.log("\nResult: PASS");
+
+		if (options.keepOnSuccess) {
+			console.log(`Keeping successful test directory: ${projectRoot}`);
+		} else {
+			await rm(sandboxRoot, { recursive: true, force: true });
+			console.log(`Deleted successful test directory: ${sandboxRoot}`);
+		}
+
+		return 0;
+	} catch (error: unknown) {
+		const message = error instanceof Error ? error.message : "Unknown error";
+
+		console.error("\nResult: FAIL");
+		console.error(message);
+
+		if (reports.length > 0) {
+			console.error("\nCompleted command steps before failure:");
+			for (const report of reports) {
+				console.error(`- ${report.step}`);
+			}
+		}
+
+		if (options.keepOnFailure) {
+			console.error(
+				`\nKept failing test directory for inspection: ${projectRoot}`,
+			);
+		} else {
+			await rm(sandboxRoot, { recursive: true, force: true });
+			console.error(`\nDeleted failing test directory: ${sandboxRoot}`);
+		}
+
+		return 1;
+	}
 }
 
 /**
  * Main entry point.
  */
 async function main(): Promise<void> {
-    try {
-        const options = parseArgs(process.argv.slice(2));
-        const exitCode = await runRoutine(options);
-        process.exit(exitCode);
-    } catch (error: unknown) {
-        const message = error instanceof Error ? error.message : "Unknown fatal error";
-        console.error(`Fatal error: ${message}`);
-        process.exit(1);
-    }
+	try {
+		const options = parseArgs(process.argv.slice(2));
+		const exitCode = await runRoutine(options);
+		process.exit(exitCode);
+	} catch (error: unknown) {
+		const message =
+			error instanceof Error ? error.message : "Unknown fatal error";
+		console.error(`Fatal error: ${message}`);
+		process.exit(1);
+	}
 }
 
-await main();
\ No newline at end of file
+await main();

--
Gitblit v1.10.0