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: +

+ + + +

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:

+ + + +

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); +}