diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml
index b5c0a0c..e890b01 100644
--- a/.github/workflows/build-test-deploy.yml
+++ b/.github/workflows/build-test-deploy.yml
@@ -49,6 +49,8 @@ jobs:
- name: Generate sitemap
run: yarn run generate-sitemap
+ - name: Create extensionless symlinks for HTML files
+ run: yarn run create-extensionless-symlinks
- name: Upload build artifact, ready for GitHub Pages deployment
uses: actions/upload-pages-artifact@v3
with:
@@ -110,6 +112,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
+ - uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
@@ -118,7 +121,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: '20'
+ node-version: "20"
- name: Install Lighthouse CI
run: npm install -g @lhci/cli@0.15.x
- name: Run Lighthouse CI
diff --git a/README.md b/README.md
index e03ed2b..cf9c2a8 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,22 @@ yarn test-structured-data
This validates that any `application/ld+json` scripts in `build/**/*.html` files have correct schema.org formats and valid JSON syntax.
+#### Extensionless Symlinks Testing
+
+Test that extensionless symlinks are correctly created for HTML files:
+
+```sh
+yarn test-extensionless-symlinks
+```
+
+This verifies that for every `file.html` in the build directory (except `index.html`), there exists a symlink `file` pointing to `file.html`. This allows URLs like `/about` to work alongside `/about.html`.
+
+You can also manually create the symlinks:
+
+```sh
+yarn create-extensionless-symlinks
+```
+
## Notes for VS Code
Open this folder in VS Code, allow the "Reopen in Container" and install recommended extensions.
diff --git a/lighthouserc.js b/lighthouserc.js
new file mode 100644
index 0000000..1d9948c
--- /dev/null
+++ b/lighthouserc.js
@@ -0,0 +1,40 @@
+module.exports = {
+ ci: {
+ collect: {
+ // Use a local static server instead of trying to analyze file:// URLs
+ staticDistDir: "./build",
+ // Specify the URLs to test (extensionless versions)
+ url: [
+ "http://localhost/index.html",
+ "http://localhost/index2.html",
+ "http://localhost/experiment-template/page-to-test",
+ "http://localhost/experiment-template/variant-1",
+ "http://localhost/experiment-template/variant-2",
+ ],
+ // Give pages time to load and render content
+ settings: {
+ chromeFlags: "--no-sandbox --disable-dev-shm-usage",
+ // Increase timeout to allow for content rendering
+ maxWaitForLoad: 30000,
+ // Wait for network to be idle before collecting metrics
+ networkQuietThresholdMs: 5000,
+ // Ensure we wait for content to render
+ waitUntil: ["load", "networkidle0"],
+ },
+ },
+ upload: {
+ target: "temporary-public-storage",
+ },
+ assert: {
+ assertions: {
+ "categories:performance": ["warn", { minScore: 0.5 }],
+ "categories:accessibility": ["error", { minScore: 0.8 }],
+ "categories:best-practices": ["warn", { minScore: 0.8 }],
+ "categories:seo": ["warn", { minScore: 0.8 }],
+ // Allow FCP to be more lenient for static content
+ "first-contentful-paint": ["warn", { maxNumericValue: 4000 }],
+ "largest-contentful-paint": ["warn", { maxNumericValue: 6000 }],
+ },
+ },
+ },
+};
diff --git a/package.json b/package.json
index a78362a..e66fc53 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,10 @@
"structured-data-testing-tool": "^4.5.0"
},
"scripts": {
- "test": "yarn node test/fixtures-html-validate-should-fail.mjs && yarn node test/fixtures-structured-data-should-fail.mjs && yarn node test/build-html-validate.mjs && yarn node test/dirty-words-checker.mjs && yarn node test/dirty-file-paths-checker.mjs && yarn node test/build-structured-data-validate.mjs",
+ "test": "yarn node test/fixtures-html-validate-should-fail.mjs && yarn node test/fixtures-structured-data-should-fail.mjs && yarn node test/build-html-validate.mjs && yarn node test/dirty-words-checker.mjs && yarn node test/dirty-file-paths-checker.mjs && yarn node test/build-structured-data-validate.mjs && yarn node test/extensionless-symlinks-checker.mjs",
"test-structured-data": "yarn node test/build-structured-data-validate.mjs",
+ "test-extensionless-symlinks": "yarn node test/extensionless-symlinks-checker.mjs",
+ "create-extensionless-symlinks": "node scripts/create-extensionless-symlinks.mjs",
"lint": "yarn prettier-check && yarn markdownlint-check",
"lint-fix": "yarn prettier-fix && yarn markdownlint-fix",
"generate-sitemap": "node scripts/generate-sitemap.mjs",
diff --git a/scripts/create-extensionless-symlinks.mjs b/scripts/create-extensionless-symlinks.mjs
new file mode 100644
index 0000000..33bda6f
--- /dev/null
+++ b/scripts/create-extensionless-symlinks.mjs
@@ -0,0 +1,82 @@
+import fs from "fs";
+import path from "path";
+
+const buildDir = path.join(process.cwd(), "build");
+
+/**
+ * Recursively find all HTML files in a directory
+ * @param {string} dir - Directory to search
+ * @param {string[]} fileList - Array to collect file paths
+ * @returns {string[]} Array of HTML file paths
+ */
+function findHtmlFiles(dir, fileList = []) {
+ const files = fs.readdirSync(dir);
+
+ files.forEach((file) => {
+ const filePath = path.join(dir, file);
+ if (fs.statSync(filePath).isDirectory()) {
+ findHtmlFiles(filePath, fileList);
+ } else if (path.extname(file) === ".html") {
+ fileList.push(filePath);
+ }
+ });
+
+ return fileList;
+}
+
+/**
+ * Create extensionless copies for HTML files
+ * Using file copies instead of symlinks for better GitHub Actions artifact compatibility
+ */
+function createExtensionlessSymlinks() {
+ console.log("๐ Creating extensionless copies for HTML files...");
+
+ if (!fs.existsSync(buildDir)) {
+ console.error(`โ Build directory does not exist: ${buildDir}`);
+ process.exit(1);
+ }
+
+ const htmlFiles = findHtmlFiles(buildDir);
+
+ if (htmlFiles.length === 0) {
+ console.log("โ ๏ธ No HTML files found in build directory");
+ return;
+ }
+
+ let filesCreated = 0;
+
+ htmlFiles.forEach((htmlFile) => {
+ const dir = path.dirname(htmlFile);
+ const basename = path.basename(htmlFile, ".html");
+
+ // Skip index.html files as they typically don't need extensionless versions
+ // (the directory itself serves as the extensionless version)
+ if (basename === "index") {
+ return;
+ }
+
+ const extensionlessPath = path.join(dir, basename);
+
+ try {
+ // Check if extensionless file already exists
+ if (fs.existsSync(extensionlessPath)) {
+ console.log(`โ ๏ธ File already exists: ${path.relative(buildDir, extensionlessPath)}`);
+ return;
+ }
+
+ // Copy the HTML file to create extensionless version
+ fs.copyFileSync(htmlFile, extensionlessPath);
+ console.log(`โ Created extensionless copy: ${path.relative(buildDir, extensionlessPath)}`);
+ filesCreated++;
+ } catch (error) {
+ console.error(
+ `โ Failed to create extensionless copy ${path.relative(buildDir, extensionlessPath)}: ${error.message}`,
+ );
+ }
+ });
+
+ console.log(`๐ Successfully created ${filesCreated} extensionless copies`);
+}
+
+// Run the script
+createExtensionlessSymlinks();
diff --git a/scripts/generate-sitemap.mjs b/scripts/generate-sitemap.mjs
index 2e9c758..010ff91 100644
--- a/scripts/generate-sitemap.mjs
+++ b/scripts/generate-sitemap.mjs
@@ -32,6 +32,12 @@ function generateSitemap(files, lastmodDate) {
// Recursive function to find HTML files in subdirectories
function getHTMLFiles(dir, fileList) {
+ // Check if directory exists
+ if (!fs.existsSync(dir)) {
+ console.log(`Build directory ${dir} does not exist. Creating empty sitemap.`);
+ return [];
+ }
+
const files = fs.readdirSync(dir);
fileList = fileList || [];
@@ -68,6 +74,26 @@ https
return;
}
+ // Check if the sitemap has the expected structure and URLs
+ if (
+ !result ||
+ !result.urlset ||
+ !result.urlset.url ||
+ !Array.isArray(result.urlset.url) ||
+ result.urlset.url.length === 0
+ ) {
+ console.log("External sitemap exists but has no URLs or unexpected structure, using current date");
+ generateAndWriteSitemap(new Date());
+ return;
+ }
+
+ // Check if the first URL has lastmod information
+ if (!result.urlset.url[0].lastmod || !result.urlset.url[0].lastmod[0]) {
+ console.log("External sitemap URLs don't have lastmod information, using current date");
+ generateAndWriteSitemap(new Date());
+ return;
+ }
+
// Get the lastmod date of the first URL
const lastmod = new Date(result.urlset.url[0].lastmod[0]);
const today = new Date();
@@ -88,6 +114,11 @@ https
// Function to generate and write sitemap
function generateAndWriteSitemap(lastmodDate) {
+ // Ensure build directory exists
+ if (!fs.existsSync(buildFolderPath)) {
+ fs.mkdirSync(buildFolderPath, { recursive: true });
+ }
+
// Find all HTML files in build folder and its subdirectories
const htmlFiles = getHTMLFiles(buildFolderPath);
// Generate sitemap XML content
diff --git a/source/experiment-template/page-to-test.html b/source/experiment-template/page-to-test.html
index a8deabf..3937868 100644
--- a/source/experiment-template/page-to-test.html
+++ b/source/experiment-template/page-to-test.html
@@ -4,5 +4,6 @@
variants:
- ./variant-1
- ./variant-2
-title: The best horse color
+title: Horse Color Preference Study - The best horse color
+description: An experimental study to determine user preferences for different horse colors through A/B testing
---
diff --git a/source/experiment-template/variant-1.html b/source/experiment-template/variant-1.html
index 9352ca8..e5142ac 100644
--- a/source/experiment-template/variant-1.html
+++ b/source/experiment-template/variant-1.html
@@ -7,11 +7,67 @@
+
{{ page.title }}
+
- Blue horses are best
+
+
Horse Color Preferences - Blue Variant
+
+
+ Blue horses are best
+
+
+
+ This is an experimental page testing user preferences for horse colors. In this variant, we're exploring the
+ appeal of blue-colored horses.
+
+
+
About Blue Horses in Art and Culture
+
+ While true blue horses don't exist in nature, they have appeared frequently in art, literature, and mythology:
+
+
+
+
Franz Marc's Blue Horses - Famous German expressionist paintings featuring blue horses
+
+ Mythology - Blue horses appear in various cultural stories as symbols of nobility and mystery
+
+
+ Modern Art - Contemporary artists often use blue horses to represent freedom and imagination
+
+
+
+
Actual Horse Colors
+
In reality, horses come in many beautiful natural colors including:
+
Bay, Chestnut, Black, Gray, Palomino, Pinto, Appaloosa, and many other variations.
+
+
+
diff --git a/source/experiment-template/variant-2.html b/source/experiment-template/variant-2.html
index ff997c8..8bcfc6a 100644
--- a/source/experiment-template/variant-2.html
+++ b/source/experiment-template/variant-2.html
@@ -7,11 +7,68 @@
+
{{ page.title }}
+
- Orange horses are best
+
+
Horse Color Preferences - Orange Variant
+
+
+ Orange horses are best
+
+
+
+ This is an experimental page testing user preferences for horse colors. In this variant, we're exploring the
+ appeal of orange-colored horses.
+
+
+
Orange-Toned Horses in Real Life
+
While pure orange doesn't occur in horse coloring, several natural colors come close to orange tones:
+
+
+
Chestnut - Ranges from light golden to deep red-brown, some with orange undertones
+
Sorrel - A reddish-brown color that can appear orange in certain lighting
+
Palomino - Golden coat color that can have orange hues, especially in sunlight
+
Liver Chestnut - Deep brown with reddish-orange highlights
+
+
+
Cultural Significance
+
Orange and warm-toned horses have special meaning in many cultures:
+
They're often associated with energy, warmth, enthusiasm, and the beauty of autumn landscapes.
+
+
Care Considerations
+
+ Horses with lighter, warmer coats like chestnuts and palominos may require extra sun protection to prevent coat
+ fading and skin damage during peak summer months.
+
+
+
+
diff --git a/source/index2.md b/source/index2.md
index 3a1cf9e..95b8e85 100644
--- a/source/index2.md
+++ b/source/index2.md
@@ -3,6 +3,30 @@
Horses
{.lead}
-Hello
+Hello, welcome to our comprehensive guide about horses and equestrian care.
Sup
+
+## About Horses
+
+Horses are magnificent creatures that have been companions to humans for thousands of years. They are intelligent, powerful, and graceful animals that deserve our respect and proper care.
+
+### Horse Care Basics
+
+- **Feeding**: Horses require a balanced diet of hay, grain, and fresh water
+- **Grooming**: Regular brushing keeps their coat healthy and builds trust
+- **Exercise**: Daily exercise is essential for physical and mental health
+- **Veterinary Care**: Regular check-ups ensure optimal health
+
+### Popular Horse Breeds
+
+1. **Arabian** - Known for their endurance and distinctive head shape
+2. **Thoroughbred** - Famous for racing and athletic ability
+3. **Quarter Horse** - Versatile breed excellent for ranch work
+4. **Clydesdale** - Large draft horses known for their strength
+
+### Getting Started with Horses
+
+If you're interested in working with horses, consider starting with riding lessons at a local stable. Many facilities offer beginner programs that teach both riding skills and horse care fundamentals.
+
+*Remember: Horses are living beings that require dedication, knowledge, and respect. Always prioritize their welfare and seek guidance from experienced horse professionals.*
diff --git a/test/extensionless-symlinks-checker.mjs b/test/extensionless-symlinks-checker.mjs
new file mode 100644
index 0000000..8d9c0b1
--- /dev/null
+++ b/test/extensionless-symlinks-checker.mjs
@@ -0,0 +1,95 @@
+import fs from "fs";
+import path from "path";
+import { execSync } from "child_process";
+
+const BUILD_DIR = path.join(process.cwd(), "build");
+
+console.log("๐งช Testing extensionless files creation");
+
+/**
+ * Test the extensionless file creation script
+ */
+function testSymlinkCreation() {
+ let hasErrors = false;
+
+ // Check if build directory exists
+ if (!fs.existsSync(BUILD_DIR)) {
+ console.error("โ Build directory does not exist. Please build the site first.");
+ process.exit(1);
+ }
+
+ // Find all HTML files
+ function findHtmlFiles(dir, fileList = []) {
+ const files = fs.readdirSync(dir);
+
+ files.forEach((file) => {
+ const filePath = path.join(dir, file);
+ if (fs.statSync(filePath).isDirectory()) {
+ findHtmlFiles(filePath, fileList);
+ } else if (path.extname(file) === ".html") {
+ fileList.push(filePath);
+ }
+ });
+
+ return fileList;
+ }
+
+ const htmlFiles = findHtmlFiles(BUILD_DIR);
+
+ if (htmlFiles.length === 0) {
+ console.log("โ ๏ธ No HTML files found in build directory");
+ return !hasErrors;
+ }
+
+ console.log(`Found ${htmlFiles.length} HTML files`);
+
+ // Test each HTML file (except index.html files)
+ htmlFiles.forEach((htmlFile) => {
+ const dir = path.dirname(htmlFile);
+ const basename = path.basename(htmlFile, ".html");
+
+ // Skip index.html files
+ if (basename === "index") {
+ return;
+ }
+
+ const extensionlessPath = path.join(dir, basename);
+
+ // Check if extensionless file exists
+ if (!fs.existsSync(extensionlessPath)) {
+ console.error(`โ Missing extensionless file: ${path.relative(BUILD_DIR, extensionlessPath)}`);
+ hasErrors = true;
+ return;
+ }
+
+ // Check if content is accessible and matches original
+ try {
+ const originalContent = fs.readFileSync(htmlFile, "utf8");
+ const extensionlessContent = fs.readFileSync(extensionlessPath, "utf8");
+
+ if (originalContent !== extensionlessContent) {
+ console.error(`โ Content mismatch in extensionless file: ${path.relative(BUILD_DIR, extensionlessPath)}`);
+ hasErrors = true;
+ return;
+ }
+
+ console.log(`โ ${path.relative(BUILD_DIR, extensionlessPath)}`);
+ } catch (error) {
+ console.error(
+ `โ Cannot read extensionless file: ${path.relative(BUILD_DIR, extensionlessPath)} - ${error.message}`,
+ );
+ hasErrors = true;
+ }
+ });
+
+ return !hasErrors;
+}
+
+// Run the test
+if (testSymlinkCreation()) {
+ console.log("โจ All extensionless files are correctly created!\n");
+ process.exit(0);
+} else {
+ console.error("\nโ Extensionless files test failed");
+ process.exit(1);
+}