Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build-test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions lighthouserc.js
Original file line number Diff line number Diff line change
@@ -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 }],
},
},
},
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
82 changes: 82 additions & 0 deletions scripts/create-extensionless-symlinks.mjs
Original file line number Diff line number Diff line change
@@ -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();
31 changes: 31 additions & 0 deletions scripts/generate-sitemap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 || [];

Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion source/experiment-template/page-to-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
58 changes: 57 additions & 1 deletion source/experiment-template/variant-1.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,67 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page.title }}</title>
<link rel="canonical" href="{{ page.canonical-link }}">
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
color: #333;
}
h1 {
color: #2c3e50;
}
.highlight {
background-color: #3498db;
color: white;
padding: 20px;
border-radius: 5px;
margin: 20px 0;
}
.content {
max-width: 800px;
}
</style>
</head>

<body>
Blue horses are best
<div class="content">
<h1>Horse Color Preferences - Blue Variant</h1>

<div class="highlight">
<strong>Blue horses are best</strong>
</div>

<p>
This is an experimental page testing user preferences for horse colors. In this variant, we're exploring the
appeal of blue-colored horses.
</p>

<h2>About Blue Horses in Art and Culture</h2>
<p>
While true blue horses don't exist in nature, they have appeared frequently in art, literature, and mythology:
</p>

<ul>
<li><strong>Franz Marc's Blue Horses</strong> - Famous German expressionist paintings featuring blue horses</li>
<li>
<strong>Mythology</strong> - Blue horses appear in various cultural stories as symbols of nobility and mystery
</li>
<li>
<strong>Modern Art</strong> - Contemporary artists often use blue horses to represent freedom and imagination
</li>
</ul>

<h2>Actual Horse Colors</h2>
<p>In reality, horses come in many beautiful natural colors including:</p>
<p><em>Bay, Chestnut, Black, Gray, Palomino, Pinto, Appaloosa, and many other variations.</em></p>

<footer style="margin-top: 40px; font-size: 0.9em; color: #666;">
This is part of an A/B testing experiment to understand color preferences.
</footer>
</div>
</body>
</html>
59 changes: 58 additions & 1 deletion source/experiment-template/variant-2.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,68 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page.title }}</title>
<link rel="canonical" href="{{ page.canonical-link }}">
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
color: #333;
}
h1 {
color: #e67e22;
}
.highlight {
background-color: #e67e22;
color: white;
padding: 20px;
border-radius: 5px;
margin: 20px 0;
}
.content {
max-width: 800px;
}
</style>
</head>

<body>
Orange horses are best
<div class="content">
<h1>Horse Color Preferences - Orange Variant</h1>

<div class="highlight">
<strong>Orange horses are best</strong>
</div>

<p>
This is an experimental page testing user preferences for horse colors. In this variant, we're exploring the
appeal of orange-colored horses.
</p>

<h2>Orange-Toned Horses in Real Life</h2>
<p>While pure orange doesn't occur in horse coloring, several natural colors come close to orange tones:</p>

<ul>
<li><strong>Chestnut</strong> - Ranges from light golden to deep red-brown, some with orange undertones</li>
<li><strong>Sorrel</strong> - A reddish-brown color that can appear orange in certain lighting</li>
<li><strong>Palomino</strong> - Golden coat color that can have orange hues, especially in sunlight</li>
<li><strong>Liver Chestnut</strong> - Deep brown with reddish-orange highlights</li>
</ul>

<h2>Cultural Significance</h2>
<p>Orange and warm-toned horses have special meaning in many cultures:</p>
<p><em>They're often associated with energy, warmth, enthusiasm, and the beauty of autumn landscapes.</em></p>

<h2>Care Considerations</h2>
<p>
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.
</p>

<footer style="margin-top: 40px; font-size: 0.9em; color: #666;">
This is part of an A/B testing experiment to understand color preferences.
</footer>
</div>
</body>
</html>
Loading
Loading