├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .nvmrc ├── .prettierrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── assets └── plugin-a11y-log.png ├── demo ├── 404.html ├── a11y-errors-page │ └── index.html ├── about │ └── index.html ├── css │ ├── colors.css │ ├── index.css │ └── prism-base16-monokai.dark.css ├── feed │ ├── .htaccess │ ├── feed.json │ └── feed.xml ├── img │ ├── .gitkeep │ ├── apple-touch-icon.png │ ├── cats-570x720.png │ ├── cats.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── img_6935_720.png ├── index.html ├── page-list │ └── index.html ├── posts │ ├── firstpost │ │ └── index.html │ ├── fourthpost │ │ └── index.html │ ├── index.html │ ├── secondpost │ │ └── index.html │ └── thirdpost │ │ └── index.html ├── sitemap.xml └── tags │ ├── another-tag │ └── index.html │ ├── index.html │ ├── number-2 │ └── index.html │ ├── posts-with-two-tags │ └── index.html │ └── second-tag │ └── index.html ├── jest.config.js ├── manifest.yml ├── netlify.toml ├── package-lock.json ├── package.json ├── src ├── config.ts ├── index.ts ├── mimeTypes.json ├── pluginCore.ts ├── reporter.ts └── server.ts ├── tests ├── generateFilePaths │ ├── __snapshots__ │ │ └── this.test.js.snap │ ├── publishDir │ │ ├── about.html │ │ ├── admin │ │ │ └── index.html │ │ ├── blog │ │ │ ├── post1.html │ │ │ └── post2.html │ │ └── index.html │ └── this.test.js └── runPa11y │ ├── __snapshots__ │ └── this.test.js.snap │ ├── publishDir │ └── index.html │ └── this.test.js └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { overrides } = require('@netlify/eslint-config-node') 2 | module.exports = { 3 | env: { 4 | node: true, 5 | }, 6 | parserOptions: { 7 | ecmaVersion: '2019', 8 | }, 9 | overrides: [...overrides], 10 | extends: 'prettier', 11 | rules: {}, 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | * **Please check if the PR fulfills these requirements** 2 | - [ ] The commit message follows our guidelines 3 | - [ ] Tests for the changes have been added (for bug fixes / features) 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | 6 | 7 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 8 | 9 | 10 | 11 | * **What is the current behavior?** (You can also link to an open issue here) 12 | 13 | 14 | 15 | * **What is the new behavior (if this is a feature change)?** 16 | 17 | 18 | 19 | * **Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) 20 | 21 | 22 | 23 | * **Other information**: 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | .env*.local 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | .parcel-cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | ### OSX ### 114 | # General 115 | .DS_Store 116 | .AppleDouble 117 | .LSOverride 118 | 119 | # Icon must end with two \r 120 | Icon 121 | 122 | 123 | # Thumbnails 124 | ._* 125 | 126 | # Files that might appear in the root of a volume 127 | .DocumentRevisions-V100 128 | .fseventsd 129 | .Spotlight-V100 130 | .TemporaryItems 131 | .Trashes 132 | .VolumeIcon.icns 133 | .com.apple.timemachine.donotpresent 134 | 135 | # Directories potentially created on remote AFP share 136 | .AppleDB 137 | .AppleDesktop 138 | Network Trash Folder 139 | Temporary Items 140 | .apdisk 141 | 142 | # End of https://www.toptal.com/developers/gitignore/api/osx,node 143 | 144 | # CLI 145 | .netlify 146 | lib -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.15 -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@netlify/eslint-config-node/.prettierrc.json'), 3 | endOfLine: 'auto', 4 | useTabs: true, 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | Prior to v1.0.0-alpha.1, logs were generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 9 | 10 | ## [v1.0.0-beta.1](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v1.0.0-alpha.6...v1.0.0-beta.1) 11 | 12 | ### Added 13 | - New input: `ignoreGuidelines`. Used to indicate the accessibility standards or guidelines that the plugin should ignoore (#76; thank-you, @amykapernick) 14 | 15 | ### Changed 16 | - **Breaking:** Now requires Node 16.0.0. 17 | - Now uses Pa11y 6.2.3 and TSLib 2.4.0. 18 | - Internal: The source code of the app is now 100% TypeScript! 19 | - Internal: Many dev dependencies have been updated. 20 | 21 | ## [v1.0.0-alpha.6](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) 22 | 23 | ### Changed 24 | - Fixes [#68](https://github.com/netlify-labs/netlify-plugin-a11y/issues/68) by only failing builds if issues are found. Thank you to [@vgautam](https://github.com/vgautam) for reporting. 25 | - Internal: Updates dev dependencies. 26 | 27 | ## [v1.0.0-alpha.5](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) 28 | 29 | ### Added 30 | - New input: `ignoreElements`. Used to indicate to the test runner elements which should be ignored during a11y testing. 31 | ### Changed 32 | - Lowers minimum required version of Node from `12.13.0` to `12.0.0`. 33 | - Internal: Uses a local server powered by the Node `http` module to host files prior to testing. This local server makes tests more accurate and more secure! 34 | ## [v1.0.0-alpha.4](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) 35 | ### Changed 36 | - Added a pre-release notice to the README, as well as other copy cleanup. 37 | - Internal: Made the plugin much more performant and memory-efficient by keeping Puppeteer alive through the run and writing our own reporter. 38 | - Internal: some redundancy when defining `directoryFilter`. 39 | 40 | ## [v1.0.0-alpha.3](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) 41 | ### Changed 42 | - Fixed colors of report printed when using `failWithIssues=true`. 43 | 44 | ## [v1.0.0-alpha.2](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) 45 | ### Changed 46 | - `standard` input is now `wcagLevel` 47 | 48 | ### Removed 49 | - `debugMode` input 50 | - `testMode` input 51 | 52 | ### Security 53 | - Dependencies have been updated 54 | ## [v1.0.0-alpha.1](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.12...v1.0.0-alpha.1) 55 | 56 | ### Changed 57 | - `resultMode` is now `failWithIssues`, a boolean defaulting to true 58 | - aXe is now the plugin's default runner 59 | 60 | ### Removed 61 | - HTML CodeSniffer is no longer supported 62 | 63 | ### Security 64 | - Dependencies have been updated. 65 | 66 | ### Merged 67 | 68 | - Bugfix: pa11y requires absolute paths for local files [`#39`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/39) 69 | - Fix issue with scanning subfolders if ignoreDirectories is not used [`#35`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/35) 70 | - Bump path-parse from 1.0.6 to 1.0.7 [`#46`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/46) 71 | - Bump ini from 1.3.5 to 1.3.8 [`#36`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/36) 72 | - Bump lodash from 4.17.19 to 4.17.21 [`#43`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/43) 73 | - Bump handlebars from 4.7.3 to 4.7.7 [`#42`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/42) 74 | - Bump dot-prop from 4.2.0 to 4.2.1 [`#38`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/38) 75 | - Update README.md [`#37`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/37) 76 | - Bump yargs-parser from 18.1.1 to 18.1.3 [`#31`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/31) 77 | - Bump node-fetch from 2.6.0 to 2.6.1 [`#33`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/33) 78 | - Bump lodash from 4.17.15 to 4.17.19 [`#29`](https://github.com/netlify-labs/netlify-plugin-a11y/pull/29) 79 | 80 | ### Commits 81 | 82 | - Add git hooks, eslint, prettier [`9f84603`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/9f846031348a0ca9a5d33efb062f65692faee296) 83 | - Update deps & engines [`cdef940`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/cdef9401b5a69a36203525ef740320b03ae1f3d1) 84 | - Switch to NPM for package management [`fc7fa28`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/fc7fa28c06e13f870261fded47544d4e92c3cb04) 85 | 86 | ## [v0.0.12](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.11...v0.0.12) - 2020-05-27 87 | 88 | ### Commits 89 | 90 | - Merge pull request #24 from ehmicky/feat/improve-default-mode [`6d0a8e4`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/6d0a8e4ba7cf1c7fef0b27b812897d9bb374f7b9) 91 | - Add default value for `checkPaths` input [`5809d5b`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/5809d5b6a7a177a91aee718a0c1d65f3d07d12a8) 92 | 93 | ## [v0.0.11](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.10...v0.0.11) - 2020-05-21 94 | 95 | ### Commits 96 | 97 | - Merge pull request #23 from papb/patch-1 [`e2fb961`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/e2fb96166cfa21d5f693c3ff952d8186b50592c4) 98 | - Merge pull request #21 from jhackshaw/ignore-directories [`edc23f3`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/edc23f36397b5e49d829083dcdb3e9a2493d8453) 99 | - allow explicitly ignoring directories [`5d05752`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/5d05752da9126d46696ba47e1f8032f49a65e574) 100 | 101 | ## [v0.0.10](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.9...v0.0.10) - 2020-05-12 102 | 103 | ### Commits 104 | 105 | - Merge pull request #22 from sw-yx/feat/error-handling [`74aa06b`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/74aa06b1dd0bdd7dbed326aca0f9a9d8c5a9fcf6) 106 | - Improve error handling [`8a7a8f2`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/8a7a8f2d2d62080602bef0b1abd3574b9d52d125) 107 | 108 | ## [v0.0.9](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.8...v0.0.9) - 2020-05-05 109 | 110 | ### Commits 111 | 112 | - Merge pull request #20 from sw-yx/fix/html-crawling [`3fc332f`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/3fc332f7459cf469dae8cc585f0355bb6c1dde29) 113 | - Fix HTML crawling [`bdbd214`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/bdbd21410e807647c1f8cb207f7642a49c11e7a1) 114 | - Validate that `checkPaths` exist [`4bd568e`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/4bd568e930b33b23e5974e55a6131831e397bee1) 115 | 116 | ## [v0.0.8](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.7...v0.0.8) - 2020-05-02 117 | 118 | ### Commits 119 | 120 | - Merge pull request #8 from ehmicky/chore-remove-plugin-name [`5f4a1ab`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/5f4a1ab276db6a54cedb700023d0422a4dc3ff10) 121 | - Merge pull request #9 from ehmicky/chore/remove-top-level-function [`6f34422`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/6f3442259f5a23914e4878af6e7bef0b39acef17) 122 | - Merge pull request #10 from ehmicky/chore/add-bugs-url [`6b90978`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/6b9097891008ce58ae1259ff2db86b07164b6833) 123 | 124 | ## [v0.0.7](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.6...v0.0.7) - 2020-04-30 125 | 126 | ### Commits 127 | 128 | - Merge pull request #7 from ehmicky/feat/fail-build [`5a64f36`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/5a64f369e37e1a2fcbb701eb75de0e3dbff0d710) 129 | - Merge pull request #6 from ehmicky/chore/update-package-lock [`f3b8c72`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/f3b8c72b43d736a7ca85c015987f40db9b60c980) 130 | - Update README.md [`9b2a456`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/9b2a456aa9dc59dd002c2934c437779f0e30d3b1) 131 | 132 | ## [v0.0.6](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.5...v0.0.6) - 2020-03-20 133 | 134 | ### Commits 135 | 136 | - yarnlock [`665d598`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/665d598c628868398ace67442fffda7f7a3c4ba7) 137 | - readme [`b7bb58a`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/b7bb58a2e2b6969b1c2f0b98735741cdd51f2a2e) 138 | - warn resultmode [`185ca9d`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/185ca9d829cef9019aabbffe615f4e90baeaa949) 139 | 140 | ## [v0.0.5](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.4...v0.0.5) - 2020-03-20 141 | 142 | ### Commits 143 | 144 | - see if this works [`fefeea8`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/fefeea8958ebe8728af454655a6c86e4396e3c65) 145 | - final commit before pivot [`9cd3c3f`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/9cd3c3f8dcace84f4cdd9b94a5c8d3efbeaf3ffd) 146 | - commit real demo [`39cff04`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/39cff044b1c544f7f395d5a69c3abd6feb61ad2d) 147 | 148 | ## [v0.0.4](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.3...v0.0.4) - 2020-03-09 149 | 150 | ### Commits 151 | 152 | - just invoke binary and trust in PATH [`55f0396`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/55f0396ce52d388c82174c85e4c09c7b115c022e) 153 | - pkgjson [`8d06bf4`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/8d06bf437a88ea673cfe51593a4c39417d9aa131) 154 | 155 | ## [v0.0.3](https://github.com/netlify-labs/netlify-plugin-a11y/compare/v0.0.2...v0.0.3) - 2020-03-09 156 | 157 | ## v0.0.2 - 2020-03-20 158 | 159 | ### Commits 160 | 161 | - see if this works [`fefeea8`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/fefeea8958ebe8728af454655a6c86e4396e3c65) 162 | - final commit before pivot [`9cd3c3f`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/9cd3c3f8dcace84f4cdd9b94a5c8d3efbeaf3ffd) 163 | - fix bug and add run utils [`96eec98`](https://github.com/netlify-labs/netlify-plugin-a11y/commit/96eec983f3c75b5bf301a1bb56979d860c980b74) 164 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | First fork this project. 4 | 5 | * git clone 6 | * npm install 7 | 8 | * git checkout -b my-fix 9 | 10 | #### fix some code... 11 | 12 | * git commit -m "added this feature" 13 | * git push origin my-fix 14 | 15 | Lastly, open a pull request on Github. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netlify-plugin-a11y 2 | 3 | Check for accessibility issues on critical pages of your Netlify website. 4 | 5 | 🚧 **Note:** This plugin is pre-release software. Until version 1.0.0 is released, its API could change at any time. If you experience unexpected behavior while using this plugin, check [the changelog](./CHANGELOG.md) for any changes you might have missed, and please feel free to report an issue or submit a pull request about any issues you encounter. 6 | 7 | ## What does this plugin do? 8 | This plugin uses [`pa11y`](https://github.com/pa11y/pa11y) (which in turn uses [`axe-core`](https://github.com/dequelabs/axe-core)) to check your Netlify project for accessibility issues. 9 | 10 | If issues are found, the plugin generates a report which provides: 11 | - the path to the HTML file in which the error was found 12 | - a description of the error with a link to the relevant [Deque University rule](https://dequeuniversity.com/rules/axe/latest) 13 | - the name of the error within the aXe API 14 | - the path to the the DOM element associated with the error 15 | - the DOM element itself 16 | - the total number of issues on the page 17 | - the sum of *all* issues across *all* pages that were checked 18 | 19 | By default, the plugin checks **all** your site's pages for violations of WCAG 2.1 level AA, and fails the site build if any a11y issues are found. 20 | ## Demo 21 | 22 | The demo site is an Eleventy blog containing some pages that have accessibility issues: https://netlify-plugin-a11y-demo.netlify.com/ 23 | 24 | - the "go home" link on [the 404 page](https://netlify-plugin-a11y-demo.netlify.app/404.html) has insufficient color contrast. 25 | - the cat photo on [the blog post](https://netlify-plugin-a11y-demo.netlify.app/404.html) doesn't have an `alt` attribute. 26 | - the textarea on [the contact page](https://netlify-plugin-a11y-demo.netlify.app/contact-me/) is missing a proper label 27 | 28 | With these errors, the logs for the demo look like this: 29 | 30 | ![Screenshot of demo site build log.](./assets/plugin-a11y-log.png) 31 |
32 | Text from screnshot of demo site build log 33 | 34 | ``` 35 | 9:49:36 PM: Results for URL: file:///opt/build/repo/demo/404.html 36 | 9:49:36 PM: • Error: ARIA hidden element must not contain focusable elements (https://dequeuniversity.com/rules/axe/4.3/aria-hidden-focus?application=axeAPI) 37 | 9:49:36 PM: ├── aria-hidden-focus 38 | 9:49:36 PM: ├── #content-not-found. > a 39 | 9:49:36 PM: └── 40 | 9:49:36 PM: • Error: Elements must have sufficient color contrast (https://dequeuniversity.com/rules/axe/4.3/color-contrast?application=axeAPI) 41 | 9:49:36 PM: ├── color-contrast 42 | 9:49:36 PM: ├── html > body > main > p > a 43 | 9:49:36 PM: └── home 44 | 9:49:36 PM: 2 Errors 45 | 9:49:36 PM: Results for URL: file:///opt/build/repo/demo/posts/2018-05-01/index.html 46 | 9:49:36 PM: • Error: Images must have alternate text (https://dequeuniversity.com/rules/axe/4.3/image-alt?application=axeAPI) 47 | 9:49:36 PM: ├── image-alt 48 | 9:49:36 PM: ├── html > body > main > div:nth-child(2) > figure > img 49 | 9:49:36 PM: └── 50 | 9:49:36 PM: Creating deploy upload records 51 | 9:49:36 PM: 1 Errors 52 | 9:49:36 PM: Results for URL: file:///opt/build/repo/demo/contact-me/index.html 53 | 9:49:36 PM: • Error: Form elements must have labels (https://dequeuniversity.com/rules/axe/4.3/label?application=axeAPI) 54 | 9:49:36 PM: ├── label 55 | 9:49:36 PM: ├── html > body > main > div:nth-child(2) > form > textarea 56 | 9:49:36 PM: └── 57 | 9:49:36 PM: 1 Errors 58 | 9:49:36 PM: 4 accessibility issues found! Check the logs for more information. 59 | ``` 60 |
61 | 62 | 63 | ## Installation via the Netlify UI 64 | To install the plugin in the Netlify UI, use this [direct in-app installation link](https://app.netlify.com/plugins/netlify-plugin-a11y/install) or go to the [plugins directory](https://app.netlify.com/plugins). 65 | 66 | When installed this way, the plugin follows its default behavior, which is to check **all** your site's pages for violations of WCAG 2.1 level AA, and fail the site build if any a11y issues are found. 67 | 68 | To change the plugin's behavior, you'll want to install it throigh your `netlify.toml` file. 69 | 70 | ## Installation via the `netlify.toml` file 71 | First, install the plugin as a dev dependency. If you're using NPM to manage your packages, run the following: 72 | ``` bash 73 | npm install --save-dev @netlify/plugin-a11y 74 | ``` 75 | 76 | If you're using Yarn, run the following: 77 | ``` bash 78 | yarn add --dev @netlify/plugin-a11y 79 | ``` 80 | 81 | Next, add `@netlify/plugin-a11y` to the plugins section of your `netlify.toml` file. 82 | 83 | ```toml 84 | [[plugins]] 85 | package = "@netlify/plugin-a11y" 86 | ``` 87 | ⚠️ In `.toml` files, whitespace is important! Make sure `package` is indented two spaces. 88 | 89 | If you want to use the plugin's default settings (check **all** pages of your site for violations of WCAG 2.1 level AA; fail the netlify build if issues are found), this is all you need to do. If you want to change the way the plugin behaves, read on to the next section. 90 | 91 | ## Configuration 92 | If you've installed the plugin via `netlify.toml`, you can add a `[[plugins.inputs]]` field to change how the plugin behaves. This table outlines the inputs the plugin accepts. All of them are optional. 93 | 94 | 95 | | Input name | Description | Value type | Possible values | Default value | 96 | | ------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | --------------------------------------------- | ------------- | 97 | | `checkPaths` | Indicates which pages of your site to check | Array of strings | Any directories or HTML files in your project | `['/']` | 98 | | `failWithIssues` | Whether the build should fail if a11y issues are found | Boolean | `true` or `false` | `true` | 99 | | `ignoreDirectories` | Directories that *should not* be checked for a11y issues | Array of strings | Any directories in your project | `[]` | 100 | | `ignoreElements` | Indicates elements that should be ignored by a11y testing | String (CSS selector) | Comma-separated string of CSS selectors | `undefined` | 101 | | `ignoreGuidelines` | Indicates guidelines and types to ignore ([pa11y docs](https://github.com/pa11y/pa11y#ignore-array)) | Array of strings | Comma-separated string of WCAG Guidlines | `[]` | 102 | | `wcagLevel` | The WCAG standard level against which pages are checked | String | `'WCAG2A'` or `'WCAGA2A'` or `'WCAG2AAA'` | `'WCAG2AA'` | 103 | 104 | Here's how these inputs can be used in `netlify.toml`, with comments to explain how each input affects the plugin's behavior: 105 | 106 | ``` toml 107 | [[plugins]] 108 | package = "@netlify/plugin-a11y" 109 | [plugins.inputs] 110 | # Check all HTML files in this project (the default behavior) 111 | checkPaths = ['/'] 112 | # Do not fail the build if a11y issues are found 113 | failWithIssues = false 114 | # Ignore all HTML files in `/admin` 115 | ignoreDirectories = ['/admin'] 116 | # Ignore any accessibility issues associated with an element matching this selector 117 | ignoreElements = '.jumbotron > h2' 118 | # Ignore any accessibility issues associated with this rule code or type 119 | ignoreGuidelines = ['WCAG2AA.Principle1.Guideline1_4.1_4_6.G17'] 120 | # Perform a11y check against WCAG 2.1 AAA 121 | wcagLevel = 'WCAG2AAA' 122 | ``` 123 | -------------------------------------------------------------------------------- /assets/plugin-a11y-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/assets/plugin-a11y-log.png -------------------------------------------------------------------------------- /demo/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netlify-plugin-a11y 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |

Content not found.

39 |

Go home.

40 | 41 | 42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /demo/a11y-errors-page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | A page with some errors 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |
39 |

A page with some errors

40 | 41 |
42 | 43 |
44 | This image doesn't have an alt attribute. 45 |
46 |
47 | 48 | 49 |
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | A page with some errors 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

netlify-plugin-a11y

20 | 25 |
26 |
27 | 28 |
29 |
30 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

31 |

This is an Eleventy project created from the eleventy-base-blog repo.

32 |
33 | 34 | 35 |
36 |

A page with some errors

37 | 38 |
39 | 40 |
41 | This image doesn't have an alt attribute. 42 |
43 |
44 | 45 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/css/colors.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Do not edit directly 3 | * Generated on Mon, 12 Apr 2021 08:37:11 GMT 4 | */ 5 | 6 | :root { 7 | --color-transparent: rgba(0, 0, 0, 0); 8 | --color-black-default: #0e1e25; 9 | --color-white: #ffffff; 10 | --color-gray-darkest: #2e3c42; 11 | --color-gray-darker: #646e73; 12 | --color-gray-dark: #7e878b; 13 | --color-gray-default: #a4aaad; 14 | --color-gray-light: #eaebec; 15 | --color-gray-lighter: #f5f5f5; 16 | --color-gray-lightest: #f9fafa; 17 | --color-teal-darkest: #0d544e; 18 | --color-teal-darker: #15847b; 19 | --color-teal-default: #00ad9f; 20 | --color-teal-lighter: #00c7b6; 21 | --color-teal-lightest: #c9eeea; 22 | --color-blue-darkest: #0f456c; 23 | --color-blue-darker: #146394; 24 | --color-blue-default: #298fc2; 25 | --color-blue-lighter: #46b6d8; 26 | --color-blue-lightest: #bce4f1; 27 | --color-gold-darkest: #66400f; 28 | --color-gold-darker: #a46819; 29 | --color-gold-default: #cc811e; 30 | --color-gold-lighter: #ffad42; 31 | --color-gold-lightest: #ffe4c2; 32 | --color-red-darkest: #8e0b30; 33 | --color-red-darker: #d32254; 34 | --color-red-default: #f22c64; 35 | --color-red-lighter: #fe7299; 36 | --color-red-lightest: #fed7e2; 37 | --color-purple-darkest: #5f1b51; 38 | --color-purple-darker: #8c1773; 39 | --color-purple-default: #bc6292; 40 | --color-purple-lighter: #db95ba; 41 | --color-purple-lightest: #f2d9e6; 42 | --color-third-party-github: #151413; 43 | --color-third-party-github-dark: #000000; 44 | --color-third-party-gitlab: #e65528; 45 | --color-third-party-gitlab-dark: #c43f17; 46 | --color-third-party-bitbucket: #0047b3; 47 | --color-third-party-bitbucket-dark: #003380; 48 | --color-dark-black-darker: #021117; 49 | --color-dark-black-default: #0e1e25; 50 | --color-dark-gray-darkest: #16262c; 51 | --color-dark-gray-darker: #1b2b32; 52 | --color-dark-gray-dark: #243238; 53 | --color-dark-gray-default: #48565b; 54 | --color-dark-gray-light: #7e878b; 55 | --color-dark-gray-lighter: #a4aaad; 56 | --color-dark-gray-lightest: #eaebec; 57 | --color-dark-teal-darkest: #083532; 58 | --color-dark-teal-darker: #15847b; 59 | --color-dark-teal-default: #00ad9f; 60 | --color-dark-teal-lighter: #00c7b6; 61 | --color-dark-teal-lightest: #5cebdf; 62 | --color-dark-blue-darkest: #092e49; 63 | --color-dark-blue-darker: #155884; 64 | --color-dark-blue-default: #1083bc; 65 | --color-dark-blue-lighter: #1fb3e0; 66 | --color-dark-blue-lightest: #5cc7eb; 67 | --color-dark-gold-darkest: #472d0b; 68 | --color-dark-gold-darker: #855414; 69 | --color-dark-gold-default: #cc811e; 70 | --color-dark-gold-lighter: #ffad42; 71 | --color-dark-gold-lightest: #ffc170; 72 | --color-dark-red-darkest: #4b061a; 73 | --color-dark-red-darker: #8e0b30; 74 | --color-dark-red-default: #d32254; 75 | --color-dark-red-lighter: #fd5383; 76 | --color-dark-red-lightest: #ff7aa0; 77 | --color-dark-purple-darkest: #401236; 78 | --color-dark-purple-darker: #83166b; 79 | --color-dark-purple-default: #bf36a1; 80 | --color-dark-purple-lighter: #e561cb; 81 | --color-dark-purple-lightest: #e58bd1; 82 | --color-dark-third-party-github: #ffffff; 83 | --color-dark-third-party-gitlab: #e65528; 84 | --color-dark-third-party-bitbucket: #0047b3; 85 | } 86 | -------------------------------------------------------------------------------- /demo/css/index.css: -------------------------------------------------------------------------------- 1 | 2 | /* Colors */ 3 | :root { 4 | --color-transparent: rgba(0, 0, 0, 0); 5 | --color-black-default: #0e1e25; 6 | --color-white: #ffffff; 7 | --color-gray-darkest: #2e3c42; 8 | --color-gray-darker: #646e73; 9 | --color-gray-dark: #7e878b; 10 | --color-gray-default: #a4aaad; 11 | --color-gray-light: #eaebec; 12 | --color-gray-lighter: #f5f5f5; 13 | --color-gray-lightest: #f9fafa; 14 | --color-teal-darkest: #0d544e; 15 | --color-teal-darker: #15847b; 16 | --color-teal-default: #00ad9f; 17 | --color-teal-lighter: #00c7b6; 18 | --color-teal-lightest: #c9eeea; 19 | --color-blue-darkest: #0f456c; 20 | --color-blue-darker: #146394; 21 | --color-blue-default: #298fc2; 22 | --color-blue-lighter: #46b6d8; 23 | --color-blue-lightest: #bce4f1; 24 | --color-gold-darkest: #66400f; 25 | --color-gold-darker: #a46819; 26 | --color-gold-default: #cc811e; 27 | --color-gold-lighter: #ffad42; 28 | --color-gold-lightest: #ffe4c2; 29 | --color-red-darkest: #8e0b30; 30 | --color-red-darker: #d32254; 31 | --color-red-default: #f22c64; 32 | --color-red-lighter: #fe7299; 33 | --color-red-lightest: #fed7e2; 34 | --color-purple-darkest: #5f1b51; 35 | --color-purple-darker: #8c1773; 36 | --color-purple-default: #bc6292; 37 | --color-purple-lighter: #db95ba; 38 | --color-purple-lightest: #f2d9e6; 39 | --color-third-party-github: #151413; 40 | --color-third-party-github-dark: #000000; 41 | --color-third-party-gitlab: #e65528; 42 | --color-third-party-gitlab-dark: #c43f17; 43 | --color-third-party-bitbucket: #0047b3; 44 | --color-third-party-bitbucket-dark: #003380; 45 | 46 | /* Color Palette: Neutral colors */ 47 | --colorBlack: var(--color-black-default); /* #0e1e25 */ 48 | --colorBlackDarker: var( 49 | --colorBlack 50 | ); /* color only changes on dark theme, we add a light version to avoid an undefined variable, just in case! */ 51 | --colorGrayDarkest: var(--color-gray-darkest); /* #2e3c42 */ 52 | --colorGrayDarker: var(--color-gray-darker); /* #646e73 */ 53 | --colorGrayDark: var(--color-gray-dark); /* #7e878b */ 54 | --colorGray: var(--color-gray-default); /* #a4aaad */ 55 | --colorGrayLight: var(--color-gray-light); /* #eaebec */ 56 | --colorGrayLighter: var(--color-gray-lighter); /* #f5f5f5 */ 57 | --colorGrayLightest: var(--color-gray-lightest); /* #f9fafa */ 58 | --colorWhite: var(--color-white); /* #ffffff */ 59 | 60 | --colorListNeutral: colorBlack, colorBlackDarker, colorGrayDarkest, 61 | colorGrayDarker, colorGrayDark, colorGray, colorGrayLight, colorGrayLighter, 62 | colorGrayLightest, colorWhite; 63 | 64 | /* Color Palette: Primary colors (teal) */ 65 | --colorTealDarkest: var(--color-teal-darkest); /* #0d544e */ 66 | --colorTealDarker: var(--color-teal-darker); /* ##15847b */ 67 | --colorTeal: var(--color-teal-default); /* ##00ad9f */ 68 | --colorTealLighter: var(--color-teal-lighter); /* #00c7b6 */ 69 | --colorTealLightest: var(--color-teal-lightest); /* #c9eeea */ 70 | 71 | /* Color Palette: Primary colors (blue) */ 72 | --colorBlueDarkest: var(--color-blue-darkest); /* #0f456c */ 73 | --colorBlueDarker: var(--color-blue-darker); /* #146394 */ 74 | --colorBlue: var(--color-blue-default); /* #298fc2 */ 75 | --colorBlueLighter: var(--color-blue-lighter); /* #46b6d8 */ 76 | --colorBlueLightest: var(--color-blue-lightest); /* #bce4f1 */ 77 | 78 | /* Color Palette: Primary colors (gold) */ 79 | --colorGoldDarkest: var(--color-gold-darkest); /* #66400f */ 80 | --colorGoldDarker: var(--color-gold-darker); /* #a46819 */ 81 | --colorGold: var(--color-gold-default); /* #cc811e */ 82 | --colorGoldLighter: var(--color-gold-lighter); /* #ffad42 */ 83 | --colorGoldLightest: var(--color-gold-lightest); /* #ffe4c2 */ 84 | 85 | /* Color Palette: Primary colors (red) */ 86 | --colorRedDarkest: var(--color-red-darkest); /* #8e0b30 */ 87 | --colorRedDarker: var(--color-red-darker); /* #d32254 */ 88 | --colorRed: var(--color-red-default); /* #f22c64 */ 89 | --colorRedLighter: var(--color-red-lighter); /* #fe7299 */ 90 | --colorRedLightest: var(--color-red-lightest); /* #fed7e2 */ 91 | 92 | /* Color Palette: Secondary colors (purple) */ 93 | --colorPurpleDarkest: var(--color-purple-darkest); /* #5f1b51 */ 94 | --colorPurpleDarker: var(--color-purple-darker); /* #8c1773 */ 95 | --colorPurple: var(--color-purple-default); /* #bc6292 */ 96 | --colorPurpleLighter: var(--color-purple-lighter); /* #db95ba */ 97 | --colorPurpleLightest: var(--color-purple-lightest); /* #f2d9e6 */ 98 | 99 | --colorListPrimary: colorTealDarkest, colorTealDarker, colorTeal, 100 | colorTealLighter, colorTealLightest, colorBlueDarkest, colorBlueDarker, 101 | colorBlue, colorBlueLighter, colorBlueLightest, colorGoldDarkest, 102 | colorGoldDarker, colorGold, colorGoldLighter, colorGoldLightest, 103 | colorRedDarkest, colorRedDarker, colorRed, colorRedLighter, colorRedLightest, 104 | colorPurpleDarkest, colorPurpleDarker, colorPurple, colorPurpleLighter, 105 | colorPurpleLightest; 106 | 107 | /* Color Palette: Assorted */ 108 | --colorTransparent: var(--color-transparent); 109 | 110 | /* Color Palette: Third party brand colors */ 111 | --colorThirdPartyGitHub: var(--color-third-party-github); /* #151413 */ 112 | --colorThirdPartyGitHubDark: var( 113 | --color-third-party-github-dark 114 | ); /* #000000 */ 115 | --colorThirdPartyGitLab: var(--color-third-party-gitlab); /* #e65528 */ 116 | --colorThirdPartyGitLabDark: var( 117 | --color-third-party-gitlab-dark 118 | ); /* #c43f17 */ 119 | --colorThirdPartyBitbucket: var(--color-third-party-bitbucket); /* #0047b3 */ 120 | --colorThirdPartyBitbucketDark: var( 121 | --color-third-party-bitbucket-dark 122 | ); /* #003380 */ 123 | --colorListThirdParty: colorThirdPartyGitHub, colorThirdPartyGitHubDark, 124 | colorThirdPartyGitLab, colorThirdPartyGitLabDark, colorThirdPartyBitbucket, 125 | colorThirdPartyBitbucketDark; 126 | /* ⚠️ ⚠️ ⚠️ End of tokens imported from @netlify/netlify-design-tokens */ 127 | 128 | /* 129 | ** Deploy, Builds and Edge handlers logs colors 130 | ** Once design sets a custom palette we should 131 | ** use hex and move variables to @netlify/netlify-design-tokens 132 | */ 133 | --colorLogBrightBlack: rgb(135, 135, 135); 134 | --colorLogRed: rgb(255, 40, 40); 135 | --colorLogBrightRed: rgb(255, 85, 85); 136 | --colorLogGreen: rgb(0, 187, 0); 137 | --colorLogBrightGreen: rgb(0, 255, 0); 138 | --colorLogYellow: rgb(187, 187, 0); 139 | --colorLogBrightYellow: rgb(255, 255, 85); 140 | --colorLogBlue: rgb(0, 134, 255); 141 | --colorLogBrightBlue: rgb(100, 190, 255); 142 | --colorLogMagenta: rgb(245, 0, 245); 143 | --colorLogBrightMagenta: rgb(255, 85, 255); 144 | --colorLogCyan: rgb(0, 187, 187); 145 | --colorLogBrightCyan: rgb(85, 255, 255); 146 | --colorLogWhite: rgb(187, 187, 187); 147 | --colorLogBrightWhite: rgb(255, 255, 255); 148 | } 149 | 150 | /* Global stylesheet */ 151 | * { 152 | box-sizing: border-box; 153 | } 154 | 155 | 156 | html { 157 | font-size: 62.5%; 158 | height: 100%; 159 | } 160 | 161 | body { 162 | display: grid; 163 | 164 | font-family: 'Mulish', -apple-system, system-ui, sans-serif; 165 | font-size: 1.6rem; 166 | 167 | grid-template-rows: auto 1fr auto; 168 | 169 | margin: 0; 170 | min-height: 100%; 171 | min-height: 100vh; 172 | min-height: -webkit-fill-available; 173 | 174 | padding: 0; 175 | } 176 | 177 | figure { 178 | padding: 0; 179 | margin: 0; 180 | } 181 | 182 | .content, 183 | .warning { 184 | margin-left: auto; 185 | margin-right: auto; 186 | max-width: 100rem; 187 | width: 95%; 188 | } 189 | 190 | .tmpl-post{ 191 | display: grid; 192 | grid-template-rows: auto 10fr auto; 193 | } 194 | 195 | .tmpl-post > .content { 196 | padding-bottom: 4rem; 197 | } 198 | 199 | .pagination { 200 | border-top: 1px dashed var(--colorGrayLight); 201 | 202 | } 203 | 204 | 205 | p:first-child { 206 | margin-top: 0; 207 | } 208 | header { 209 | border-bottom: 1px dashed var(--colorGrayLight); 210 | } 211 | header:after { 212 | content: ""; 213 | display: table; 214 | clear: both; 215 | } 216 | 217 | header + * { 218 | margin-top: 1rem; 219 | } 220 | 221 | p:last-child { 222 | margin-bottom: 0; 223 | } 224 | p, 225 | .tmpl-post li, 226 | img { 227 | max-width: 37.5em; /* 600px /16 */ 228 | max-width: 70ch; 229 | } 230 | p, 231 | .tmpl-post li { 232 | line-height: 1.45; 233 | } 234 | a[href] { 235 | color: var(--colorTealDarker); 236 | } 237 | a[href]:visited { 238 | color: var(--colorTealDarkest); 239 | } 240 | table { 241 | margin: 1em 0; 242 | } 243 | table td, 244 | table th { 245 | padding-right: 1em; 246 | } 247 | 248 | pre, 249 | code { 250 | font-family: Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, monospace; 251 | line-height: 1.5; 252 | } 253 | pre { 254 | font-size: 14px; 255 | line-height: 1.375; 256 | direction: ltr; 257 | text-align: left; 258 | white-space: pre; 259 | word-spacing: normal; 260 | word-break: normal; 261 | -moz-tab-size: 2; 262 | -o-tab-size: 2; 263 | tab-size: 2; 264 | -webkit-hyphens: none; 265 | -moz-hyphens: none; 266 | -ms-hyphens: none; 267 | hyphens: none; 268 | padding: 1em; 269 | margin: .5em 0; 270 | background-color: #f6f6f6; 271 | } 272 | code { 273 | word-break: break-all; 274 | } 275 | .highlight-line { 276 | display: block; 277 | padding: 0.125em 1em; 278 | text-decoration: none; /* override del, ins, mark defaults */ 279 | color: inherit; /* override del, ins, mark defaults */ 280 | } 281 | 282 | /* allow highlighting empty lines */ 283 | .highlight-line:empty:before { 284 | content: " "; 285 | } 286 | /* avoid double line breaks when using display: block; */ 287 | .highlight-line + br { 288 | display: none; 289 | } 290 | 291 | .highlight-line-isdir { 292 | color: #b0b0b0; 293 | background-color: #222; 294 | } 295 | .highlight-line-active { 296 | background-color: #444; 297 | background-color: hsla(0, 0%, 27%, .8); 298 | } 299 | .highlight-line-add { 300 | background-color: #45844b; 301 | } 302 | .highlight-line-remove { 303 | background-color: #902f2f; 304 | } 305 | 306 | /* Header */ 307 | .home { 308 | padding: 0 1rem; 309 | float: left; 310 | margin: 1.6rem 0; /* 16px /10 */ 311 | font-size: 1em; /* 16px /16 */ 312 | } 313 | .home :link:not(:hover) { 314 | text-decoration: none; 315 | } 316 | 317 | /* Nav */ 318 | .nav { 319 | padding: 0; 320 | list-style: none; 321 | float: left; 322 | margin-left: 1em; 323 | } 324 | .nav-item { 325 | display: inline-block; 326 | margin-right: 1em; 327 | } 328 | .nav-item a[href]:not(:hover) { 329 | text-decoration: none; 330 | } 331 | .nav-item-active { 332 | font-weight: 700; 333 | text-decoration: underline; 334 | } 335 | 336 | /* Posts list */ 337 | .postlist { 338 | list-style: none; 339 | padding: 0; 340 | } 341 | .postlist-item { 342 | display: flex; 343 | flex-wrap: wrap; 344 | align-items: baseline; 345 | counter-increment: start-from -1; 346 | line-height: 1.8; 347 | } 348 | .postlist-item:before { 349 | display: inline-block; 350 | pointer-events: none; 351 | content: "" counter(start-from, decimal-leading-zero) ". "; 352 | line-height: 100%; 353 | text-align: right; 354 | } 355 | .postlist-date, 356 | .postlist-item:before { 357 | font-size: 0.8125em; /* 13px /16 */ 358 | color: var(--colorGrayDarker); 359 | } 360 | .postlist-date { 361 | word-spacing: -0.5px; 362 | } 363 | .postlist-link { 364 | padding-left: .25em; 365 | padding-right: .25em; 366 | text-underline-position: from-font; 367 | text-underline-offset: 0; 368 | text-decoration-thickness: 1px; 369 | } 370 | .postlist-item-active .postlist-link { 371 | font-weight: bold; 372 | } 373 | .tmpl-home .postlist-link { 374 | font-size: 1.1875em; /* 19px /16 */ 375 | font-weight: 700; 376 | } 377 | 378 | 379 | /* Tags */ 380 | .post-tag { 381 | display: inline-flex; 382 | align-items: center; 383 | justify-content: center; 384 | text-transform: uppercase; 385 | font-size: 0.75em; /* 12px /16 */ 386 | padding: 0.08333333333333em 0.3333333333333em; /* 1px 4px /12 */ 387 | margin-left: 0.6666666666667em; /* 8px /12 */ 388 | margin-top: 0.5em; /* 6px /12 */ 389 | margin-bottom: 0.5em; /* 6px /12 */ 390 | color: var(--colorGrayDark); 391 | border: 1px solid var(--colorGray); 392 | border-radius: 0.25em; /* 3px /12 */ 393 | text-decoration: none; 394 | line-height: 1.8; 395 | } 396 | a[href].post-tag, 397 | a[href].post-tag:visited { 398 | color: inherit; 399 | } 400 | a[href].post-tag:hover, 401 | a[href].post-tag:focus { 402 | background-color: var(--colorGrayLight); 403 | } 404 | .postlist-item > .post-tag { 405 | align-self: center; 406 | } 407 | 408 | /* Warning */ 409 | .warning { 410 | border-radius: 4px; 411 | background-color: var(--colorGoldLightest); 412 | padding: 1em 0.625em; /* 16px 10px /16 */ 413 | } 414 | 415 | .warning a[href] { 416 | color: var(--colorBlueDarker); 417 | } 418 | .warning ol:only-child { 419 | margin: 0; 420 | } 421 | 422 | /* Direct Links / Markdown Headers */ 423 | .direct-link { 424 | font-family: sans-serif; 425 | text-decoration: none; 426 | font-style: normal; 427 | margin-left: .1em; 428 | } 429 | a[href].direct-link, 430 | a[href].direct-link:visited { 431 | color: transparent; 432 | } 433 | a[href].direct-link:focus, 434 | a[href].direct-link:focus:visited, 435 | :hover > a[href].direct-link, 436 | :hover > a[href].direct-link:visited { 437 | color: #aaa; 438 | } 439 | -------------------------------------------------------------------------------- /demo/css/prism-base16-monokai.dark.css: -------------------------------------------------------------------------------- 1 | code[class*="language-"], pre[class*="language-"] { 2 | font-size: 14px; 3 | line-height: 1.375; 4 | direction: ltr; 5 | text-align: left; 6 | white-space: pre; 7 | word-spacing: normal; 8 | word-break: normal; 9 | -moz-tab-size: 2; 10 | -o-tab-size: 2; 11 | tab-size: 2; 12 | -webkit-hyphens: none; 13 | -moz-hyphens: none; 14 | -ms-hyphens: none; 15 | hyphens: none; 16 | background: #272822; 17 | color: #f8f8f2; 18 | } 19 | pre[class*="language-"] { 20 | padding: 1.5em 0; 21 | margin: .5em 0; 22 | overflow: auto; 23 | } 24 | :not(pre) > code[class*="language-"] { 25 | padding: .1em; 26 | border-radius: .3em; 27 | } 28 | .token.comment, .token.prolog, .token.doctype, .token.cdata { 29 | color: #75715e; 30 | } 31 | .token.punctuation { 32 | color: #f8f8f2; 33 | } 34 | .token.namespace { 35 | opacity: .7; 36 | } 37 | .token.operator, .token.boolean, .token.number { 38 | color: #fd971f; 39 | } 40 | .token.property { 41 | color: #f4bf75; 42 | } 43 | .token.tag { 44 | color: #66d9ef; 45 | } 46 | .token.string { 47 | color: #a1efe4; 48 | } 49 | .token.selector { 50 | color: #ae81ff; 51 | } 52 | .token.attr-name { 53 | color: #fd971f; 54 | } 55 | .token.entity, .token.url, .language-css .token.string, .style .token.string { 56 | color: #a1efe4; 57 | } 58 | .token.attr-value, .token.keyword, .token.control, .token.directive, .token.unit { 59 | color: #a6e22e; 60 | } 61 | .token.statement, .token.regex, .token.atrule { 62 | color: #a1efe4; 63 | } 64 | .token.placeholder, .token.variable { 65 | color: #66d9ef; 66 | } 67 | .token.deleted { 68 | text-decoration: line-through; 69 | } 70 | .token.inserted { 71 | border-bottom: 1px dotted #f9f8f5; 72 | text-decoration: none; 73 | } 74 | .token.italic { 75 | font-style: italic; 76 | } 77 | .token.important, .token.bold { 78 | font-weight: bold; 79 | } 80 | .token.important { 81 | color: #f92672; 82 | } 83 | .token.entity { 84 | cursor: help; 85 | } 86 | pre > code.highlight { 87 | outline: 0.4em solid #f92672; 88 | outline-offset: .4em; 89 | } 90 | -------------------------------------------------------------------------------- /demo/feed/.htaccess: -------------------------------------------------------------------------------- 1 | # For Apache, to show `feed.xml` when browsing to directory /feed/ (hide the file!) 2 | DirectoryIndex feed.xml 3 | -------------------------------------------------------------------------------- /demo/feed/feed.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1.1", 3 | "title": "netlify-plugin-a11y", 4 | "language": "en", 5 | "home_page_url": "https://example.com/", 6 | "feed_url": "https://example.com/feed/feed.json", 7 | "description": "A demo site to illustrate the funcionality of netlify-plugin-a11y.", 8 | "author": { 9 | "name": "Amberley Romo & EJ Mason", 10 | "url": "https://example.com/about-me/" 11 | }, 12 | "items": [{ 13 | "id": "https://example.com/posts/fourthpost/", 14 | "url": "https://example.com/posts/fourthpost/", 15 | "title": "This is my fourth post.", 16 | "content_html": "

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

\n

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

\n

Section Header #

\n

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

\n", 17 | "date_published": "2018-09-30T00:00:00Z" 18 | },{ 19 | "id": "https://example.com/posts/thirdpost/", 20 | "url": "https://example.com/posts/thirdpost/", 21 | "title": "This is my third post.", 22 | "content_html": "

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

\n
// this is a command
function myCommand() {
\tlet counter = 0;

\tcounter++;

}

// Test with a line break above this line.
console.log('Test');
\n

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

\n

Section Header #

\n

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

\n", 23 | "date_published": "2018-08-24T00:00:00Z" 24 | },{ 25 | "id": "https://example.com/posts/secondpost/", 26 | "url": "https://example.com/posts/secondpost/", 27 | "title": "This is my second post.", 28 | "content_html": "

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

\n

Section Header #

\n

First post
\nThird post

\n

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

\n

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

\n", 29 | "date_published": "2018-07-04T00:00:00Z" 30 | },{ 31 | "id": "https://example.com/posts/firstpost/", 32 | "url": "https://example.com/posts/firstpost/", 33 | "title": "This is my first post.", 34 | "content_html": "

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

\n

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

\n

Section Header #

\n

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

\n
// this is a command
function myCommand() {
\tlet counter = 0;
\tcounter++;
}

// Test with a line break above this line.
console.log('Test');
\n", 35 | "date_published": "2018-05-01T00:00:00Z" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /demo/feed/feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | netlify-plugin-a11y 4 | A demo site to illustrate the funcionality of netlify-plugin-a11y. 5 | 6 | 7 | 8 | 2018-09-30T00:00:00Z 9 | https://example.com/ 10 | 11 | Amberley Romo & EJ Mason 12 | youremailaddress@example.com 13 | 14 | 15 | 16 | This is my fourth post. 17 | 18 | 2018-09-30T00:00:00Z 19 | https://example.com/posts/fourthpost/ 20 | <p>Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.</p> 21 | <p>Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.</p> 22 | <h2 id="section-header" tabindex="-1">Section Header <a class="direct-link" href="https://example.com/posts/fourthpost/#section-header" aria-hidden="true">#</a></h2> 23 | <p>Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.</p> 24 | 25 | 26 | 27 | 28 | This is my third post. 29 | 30 | 2018-08-24T00:00:00Z 31 | https://example.com/posts/thirdpost/ 32 | <p>Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.</p> 33 | <pre class="language-js"><code class="language-js"><span class="highlight-line"><span class="token comment">// this is a command</span></span><br /><span class="highlight-line"><span class="token keyword">function</span> <span class="token function">myCommand</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span></span><br /><ins class="highlight-line highlight-line-add"> <span class="token keyword">let</span> counter <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span></ins><br /><span class="highlight-line"></span><br /><del class="highlight-line highlight-line-remove"> counter<span class="token operator">++</span><span class="token punctuation">;</span></del><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"><span class="token comment">// Test with a line break above this line.</span></span><br /><span class="highlight-line">console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Test'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre> 34 | <p>Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.</p> 35 | <h2 id="section-header" tabindex="-1">Section Header <a class="direct-link" href="https://example.com/posts/thirdpost/#section-header" aria-hidden="true">#</a></h2> 36 | <p>Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.</p> 37 | 38 | 39 | 40 | 41 | This is my second post. 42 | 43 | 2018-07-04T00:00:00Z 44 | https://example.com/posts/secondpost/ 45 | <p>Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.</p> 46 | <h2 id="section-header" tabindex="-1">Section Header <a class="direct-link" href="https://example.com/posts/secondpost/#section-header" aria-hidden="true">#</a></h2> 47 | <p><a href="https://example.com/posts/firstpost/">First post</a><br /> 48 | <a href="https://example.com/posts/thirdpost/">Third post</a></p> 49 | <p>Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.</p> 50 | <p>Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.</p> 51 | 52 | 53 | 54 | 55 | This is my first post. 56 | 57 | 2018-05-01T00:00:00Z 58 | https://example.com/posts/firstpost/ 59 | <p>Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.</p> 60 | <p>Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.</p> 61 | <h2 id="section-header" tabindex="-1">Section Header <a class="direct-link" href="https://example.com/posts/firstpost/#section-header" aria-hidden="true">#</a></h2> 62 | <p>Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.</p> 63 | <pre class="language-text"><code class="language-text"><span class="highlight-line">// this is a command</span><br /><span class="highlight-line">function myCommand() {</span><br /><mark class="highlight-line highlight-line-active"> let counter = 0;</mark><br /><mark class="highlight-line highlight-line-active"> counter++;</mark><br /><span class="highlight-line">}</span><br /><span class="highlight-line"></span><br /><span class="highlight-line">// Test with a line break above this line.</span><br /><span class="highlight-line">console.log('Test');</span></code></pre> 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /demo/img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/demo/img/.gitkeep -------------------------------------------------------------------------------- /demo/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/demo/img/apple-touch-icon.png -------------------------------------------------------------------------------- /demo/img/cats-570x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/demo/img/cats-570x720.png -------------------------------------------------------------------------------- /demo/img/cats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/demo/img/cats.png -------------------------------------------------------------------------------- /demo/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/demo/img/favicon-16x16.png -------------------------------------------------------------------------------- /demo/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/demo/img/favicon-32x32.png -------------------------------------------------------------------------------- /demo/img/img_6935_720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/netlify-plugin-a11y/7f8a65fc7d2487a2280a5671222e7c04f2938813/demo/img/img_6935_720.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netlify-plugin-a11y 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 | 39 |
40 |

Latest 3 Posts

41 | 42 | 43 | 44 |
    45 | 46 |
  1. 47 | This is my fourth post. 48 | 49 | 50 | 51 | 52 | 53 |
  2. 54 | 55 |
  3. 56 | This is my third post. 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
  4. 66 | 67 |
  5. 68 | This is my second post. 69 | 70 | 71 | 72 | 73 | 74 |
  6. 75 | 76 |
77 | 78 | 79 |

More posts can be found in the archive.

80 | 81 |
82 | 83 | 84 |
85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /demo/page-list/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netlify-plugin-a11y 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
URLPage Title
/posts/firstpost/This is my first post.
/posts/secondpost/This is my second post.
/posts/thirdpost/This is my third post.
/posts/fourthpost/This is my fourth post.
/a11y-errors-page/A page with some errors
/posts/
/
/tags/
/tags/another-tag/Tagged “another tag”
/tags/second-tag/Tagged “second tag”
/tags/posts-with-two-tags/Tagged “posts with two tags”
/tags/number-2/Tagged “number 2”
96 | 97 | 98 |
99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /demo/posts/firstpost/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | This is my first post. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |
39 |

This is my first post.

40 | 41 |

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

42 |

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

43 |

Section Header

44 |

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

45 |
// this is a command
function myCommand() {
let counter = 0;
counter++;
}

// Test with a line break above this line.
console.log('Test');
46 | 47 |
48 | 52 | 53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /demo/posts/fourthpost/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | This is my fourth post. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |
39 |

This is my fourth post.

40 | 41 |

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

42 |

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

43 |

Section Header

44 |

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

45 | 46 |
47 | 51 | 52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /demo/posts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netlify-plugin-a11y 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 | 39 |
40 |

Archive

41 | 42 | 43 |
    44 | 45 |
  1. 46 | This is my fourth post. 47 | 48 | 49 | 50 | 51 | 52 |
  2. 53 | 54 |
  3. 55 | This is my third post. 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
  4. 65 | 66 |
  5. 67 | This is my second post. 68 | 69 | 70 | 71 | 72 | 73 |
  6. 74 | 75 |
  7. 76 | This is my first post. 77 | 78 | 79 | 80 | 81 | 82 |
  8. 83 | 84 |
85 | 86 |
87 | 88 | 89 |
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /demo/posts/secondpost/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | This is my second post. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |
39 |

This is my second post.

40 | 41 |

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

42 |

Section Header

43 |

First post
44 | Third post

45 |

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

46 |

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

47 | 48 |
49 | 53 | 54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /demo/posts/thirdpost/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | This is my third post. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |
39 |

This is my third post.

40 | 41 |

Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.

42 |
// this is a command
function myCommand() {
let counter = 0;

counter++;

}

// Test with a line break above this line.
console.log('Test');
43 |

Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring.

44 |

Section Header

45 |

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

46 | 47 |
48 | 52 | 53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /demo/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | https://example.com/posts/firstpost/ 6 | 2018-05-01 7 | 8 | 9 | 10 | https://example.com/posts/secondpost/ 11 | 2018-07-04 12 | 13 | 14 | 15 | https://example.com/posts/thirdpost/ 16 | 2018-08-24 17 | 18 | 19 | 20 | https://example.com/posts/fourthpost/ 21 | 2018-09-30 22 | 23 | 24 | 25 | https://example.com/a11y-errors-page/ 26 | 2021-09-21 27 | 28 | 29 | 30 | https://example.com/posts/ 31 | 2021-09-21 32 | 33 | 34 | 35 | https://example.com/ 36 | 2021-09-21 37 | 38 | 39 | 40 | https://example.com/page-list/ 41 | 2021-09-21 42 | 43 | 44 | 45 | https://example.com/tags/ 46 | 2021-09-21 47 | 48 | 49 | 50 | https://example.com/tags/another-tag/ 51 | 2021-09-21 52 | 53 | 54 | 55 | https://example.com/tags/second-tag/ 56 | 2021-09-21 57 | 58 | 59 | 60 | https://example.com/tags/posts-with-two-tags/ 61 | 2021-09-21 62 | 63 | 64 | 65 | https://example.com/tags/number-2/ 66 | 2021-09-21 67 | 68 | 69 | -------------------------------------------------------------------------------- /demo/tags/another-tag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tagged “another tag” 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |

Tagged “another tag”

39 | 40 | 41 |
    42 | 43 |
  1. 44 | This is my first post. 45 | 46 | 47 | 48 | 49 | 50 |
  2. 51 | 52 |
53 | 54 | 55 |

See all tags.

56 | 57 | 58 |
59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /demo/tags/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netlify-plugin-a11y 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |

Tags

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /demo/tags/number-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tagged “number 2” 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |

Tagged “number 2”

39 | 40 | 41 |
    42 | 43 |
  1. 44 | This is my second post. 45 | 46 | 47 | 48 | 49 | 50 |
  2. 51 | 52 |
53 | 54 | 55 |

See all tags.

56 | 57 | 58 |
59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /demo/tags/posts-with-two-tags/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tagged “posts with two tags” 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |

Tagged “posts with two tags”

39 | 40 | 41 |
    42 | 43 |
  1. 44 | This is my third post. 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
  2. 54 | 55 |
56 | 57 | 58 |

See all tags.

59 | 60 | 61 |
62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /demo/tags/second-tag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tagged “second tag” 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

netlify-plugin-a11y

23 | 28 |
29 |
30 | 31 |
32 |
33 |

This site is a testing ground for netlify-plugin-a11y. It has a few intentional accessibility errors!

34 |

This is an Eleventy project created from the eleventy-base-blog repo.

35 |
36 | 37 | 38 |

Tagged “second tag”

39 | 40 | 41 |
    42 | 43 |
  1. 44 | This is my fourth post. 45 | 46 | 47 | 48 | 49 | 50 |
  2. 51 | 52 |
  3. 53 | This is my third post. 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
  4. 63 | 64 |
65 | 66 | 67 |

See all tags.

68 | 69 | 70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | } 6 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | name: netlify-plugin-a11y 2 | inputs: 3 | - name: checkPaths 4 | default: ['/'] 5 | description: Array of paths (folders or html files) that the plugin should check. Defaults to root (['/']), which checks every html file on your site. 6 | - name: failWithIssues 7 | default: true 8 | description: Whether the build should faul if the plugin finds a11y issues. Defaults to true. 9 | - name: ignoreDirectories 10 | default: [] 11 | description: Array of directories whose pages the plugin should ignore when checking for a11y issues. Defaults to []. 12 | - name: ignoreElements 13 | description: A CSS selector to ignore elements when testing. Accepts multiple comma-separated selectors. 14 | - name: ignoreGuidelines 15 | description: Ignore any accessibility issues associated with this rule code or type. Accepts multiple comma-separated rules. 16 | - name: wcagLevel 17 | default: 'WCAG2AA' 18 | description: The level of WCAG 2.1 against which to check site pages. Defaults to 'WCAGAA'; can also be 'WCAGA' or 'WCAGAAA'. 19 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base="/" 3 | command = "echo 'hi'" 4 | publish = "demo" 5 | 6 | [[plugins]] 7 | package = "./lib/index.js" 8 | [plugins.inputs] 9 | checkPaths = ['/'] 10 | ignoreDirectories = ['thirdpost'] 11 | ignoreElements = '.direct-link' 12 | failWithIssues = false # true by default 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@netlify/plugin-a11y", 3 | "version": "1.0.0-beta.1", 4 | "description": "Check for accessibility errors on critical pages of your Netlify website.", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/**/*", 8 | "manifest.yml" 9 | ], 10 | "dependencies": { 11 | "pa11y": "^6.2.3", 12 | "path-type": "^4.0.0", 13 | "picocolors": "^1.0.0", 14 | "puppeteer": "~9.1.1", 15 | "readdirp": "^3.6.0", 16 | "tslib": "^2.4.0" 17 | }, 18 | "scripts": { 19 | "build": "tsc", 20 | "watch": "tsc --watch", 21 | "test": "jest", 22 | "test:staged": "CI=true jest --findRelatedTests", 23 | "clean": "rimraf lib", 24 | "prepare": "husky install && npm run build", 25 | "prepublishOnly": "npm ci && npm test && npm run clean && npm run build" 26 | }, 27 | "keywords": [ 28 | "netlify", 29 | "netlify-plugin", 30 | "accessibility", 31 | "a11y" 32 | ], 33 | "author": "swyx ", 34 | "contributors": [ 35 | "EJ Mason " 36 | ], 37 | "license": "MIT", 38 | "engines": { 39 | "node": ">= 16.0.0", 40 | "npm": ">= 7.10.0" 41 | }, 42 | "devDependencies": { 43 | "@netlify/build": "^27.1.4", 44 | "@netlify/eslint-config-node": "^6.0.0", 45 | "@types/jest": "^28.1.1", 46 | "eslint": "^8.17.0", 47 | "eslint-config-prettier": "^8.5.0", 48 | "husky": "^8.0.1", 49 | "jest": "^28.1.1", 50 | "lint-staged": "^13.0.1", 51 | "prettier": "^2.7.0", 52 | "rimraf": "^3.0.2", 53 | "ts-jest": "^28.0.5", 54 | "typescript": "^4.7.3" 55 | }, 56 | "repository": "https://github.com/netlify-labs/netlify-plugin-a11y", 57 | "bugs": { 58 | "url": "https://github.com/netlify-labs/netlify-plugin-a11y/issues" 59 | }, 60 | "lint-staged": { 61 | "src/**/*.js": [ 62 | "npm run test:staged" 63 | ], 64 | "*.js": [ 65 | "eslint --cache --fix", 66 | "prettier --write" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer' 2 | import type { NetlifyPluginOptions } from '@netlify/build' 3 | 4 | export type WCAGLevel = 'WCAG2A' | 'WCAG2AA' | 'WCAG2AAA' 5 | 6 | type InputT = { 7 | checkPaths?: string[], 8 | ignoreDirectories?: string[], 9 | ignoreElements?: string, 10 | ignoreGuidelines?: string[], 11 | failWithIssues?: boolean, 12 | wcagLevel?: WCAGLevel, 13 | } 14 | 15 | const DEFAULT_CHECK_PATHS = ['/'] 16 | const DEFAULT_FAIL_WITH_ISSUES = true 17 | const DEFAULT_IGNORE_DIRECTORIES: string[] = [] 18 | 19 | const PA11Y_DEFAULT_WCAG_LEVEL = 'WCAG2AA' 20 | const PA11Y_RUNNERS = ['axe'] 21 | const PA11Y_USER_AGENT = 'netlify-plugin-a11y' 22 | 23 | export const getConfiguration = async ({ 24 | constants: { PUBLISH_DIR }, 25 | inputs, 26 | }: Pick) => { 27 | const { checkPaths, ignoreDirectories, ignoreElements, ignoreGuidelines, failWithIssues, wcagLevel } = 28 | inputs as InputT 29 | return { 30 | checkPaths: checkPaths || DEFAULT_CHECK_PATHS, 31 | failWithIssues: failWithIssues ?? DEFAULT_FAIL_WITH_ISSUES, 32 | ignoreDirectories: ignoreDirectories || DEFAULT_IGNORE_DIRECTORIES, 33 | pa11yOpts: await getPa11yOpts({ 34 | hideElements: ignoreElements, 35 | ignore: ignoreGuidelines, 36 | standard: wcagLevel || PA11Y_DEFAULT_WCAG_LEVEL, 37 | }), 38 | publishDir: (PUBLISH_DIR || process.env.PUBLISH_DIR) as string, 39 | } 40 | } 41 | 42 | export type Config = ReturnType 43 | 44 | export const getPa11yOpts = async ({ hideElements, ignore, standard }: { hideElements?: string; ignore?: string[]; standard: WCAGLevel }) => { 45 | return { 46 | browser: await puppeteer.launch({ ignoreHTTPSErrors: true }), 47 | hideElements, 48 | ignore, 49 | runners: PA11Y_RUNNERS, 50 | userAgent: PA11Y_USER_AGENT, 51 | standard, 52 | } 53 | } 54 | 55 | export type Pa11yOpts = Awaited> 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getConfiguration } from './config' 2 | import { generateFilePaths, runPa11y } from './pluginCore' 3 | import pico from 'picocolors' 4 | 5 | import type { OnPostBuild } from '@netlify/build' 6 | 7 | module.exports = { 8 | async onPostBuild({ constants, inputs, utils: { build }, }) { 9 | try { 10 | const { publishDir, checkPaths, ignoreDirectories, failWithIssues, pa11yOpts } = await getConfiguration({ 11 | constants, 12 | inputs, 13 | }) 14 | const htmlFilePaths = await generateFilePaths({ 15 | publishDir, 16 | ignoreDirectories, 17 | fileAndDirPaths: checkPaths, 18 | }) 19 | 20 | console.log('Checking your pages. This may take a while...') 21 | 22 | const { report, issueCount } = await runPa11y({ 23 | build, 24 | htmlFilePaths, 25 | publishDir, 26 | pa11yOpts, 27 | }) 28 | const reportSummary = 29 | `${issueCount === 0 ? 'No' : issueCount} accessibility issues found!` + 30 | (issueCount > 0 ? ' Check the logs for more information.' : '') 31 | 32 | console.log(report) 33 | 34 | if (failWithIssues && issueCount > 0) { 35 | build.failBuild(reportSummary) 36 | } else { 37 | console.warn(pico.magenta(reportSummary)) 38 | } 39 | } catch (err) { 40 | build.failBuild(err.message) 41 | } 42 | }, 43 | } as { 44 | onPostBuild: OnPostBuild 45 | } 46 | -------------------------------------------------------------------------------- /src/mimeTypes.json: -------------------------------------------------------------------------------- 1 | { 2 | ".html":"text/html", 3 | ".js":"text/javascript", 4 | ".mjs":"text/javascript", 5 | ".json":"application/json", 6 | ".css":"text/css", 7 | ".ico":"image/x-icon", 8 | ".gif":"image/gif", 9 | ".png":"image/png", 10 | ".jpg":"image/jpeg", 11 | ".jpeg":"image/jpeg", 12 | ".webp":"image/webp", 13 | ".bmp":"image/bmp", 14 | ".svg":"image/svg+xml", 15 | ".wav":"audio/wav", 16 | ".mp3":"audio/mpeg", 17 | ".mp4":"video/mpeg", 18 | ".weba":"audio/wav", 19 | ".webm":"video/wav", 20 | ".pdf":"application/pdf", 21 | ".otf":"font/otf", 22 | ".woff":"font/woff", 23 | ".ttf":"font/ttf", 24 | ".woff2":"font/woff2" 25 | } 26 | -------------------------------------------------------------------------------- /src/pluginCore.ts: -------------------------------------------------------------------------------- 1 | import pa11y from 'pa11y' 2 | import { extname, join } from 'path' 3 | import { isDirectory, isFile } from 'path-type' 4 | import { results as cliReporter } from './reporter' 5 | import readdirp from 'readdirp' 6 | import { StaticServer, SERVER_ADDRESS } from './server' 7 | 8 | import type { NetlifyPluginUtils } from '@netlify/build' 9 | import type { Pa11yOpts } from './config' 10 | 11 | const EMPTY_ARRAY = [] 12 | const ASTERISK = '*'; 13 | const HTML_EXT = '.html' 14 | const GLOB_HTML = '*.html' 15 | 16 | export const runPa11y = async function ({ 17 | build, 18 | htmlFilePaths, 19 | pa11yOpts, 20 | publishDir, 21 | }: { 22 | build: NetlifyPluginUtils['build'] 23 | htmlFilePaths: string[] 24 | pa11yOpts: Pa11yOpts 25 | publishDir: string 26 | }) { 27 | let issueCount = 0 28 | 29 | const staticServer = new StaticServer(publishDir).listen() 30 | 31 | const results = await Promise.all( 32 | htmlFilePaths.map(async (filePath) => { 33 | try { 34 | const res = await pa11y(join(SERVER_ADDRESS, filePath), pa11yOpts) 35 | if (res.issues.length) { 36 | issueCount += res.issues.length 37 | return cliReporter(res) 38 | } 39 | } catch (error) { 40 | build.failBuild('pa11y failed', { error }) 41 | } 42 | }), 43 | ) 44 | 45 | staticServer.close() 46 | 47 | await pa11yOpts.browser.close() 48 | 49 | return { 50 | issueCount, 51 | report: results.join(''), 52 | } 53 | } 54 | 55 | export const generateFilePaths = async function ({ 56 | fileAndDirPaths, // array, mix of html and directories 57 | ignoreDirectories, 58 | publishDir, 59 | }: { 60 | fileAndDirPaths: string[] 61 | ignoreDirectories: string[] 62 | publishDir: string 63 | }) { 64 | const directoryFilter = 65 | ignoreDirectories.length === 0 66 | ? ASTERISK 67 | : ignoreDirectories.map( 68 | // add ! and strip leading and trailing slashes 69 | (dir) => `!${dir.replace(/^\/|\/$/g, '')}`, 70 | ) 71 | const htmlFilePaths = await Promise.all( 72 | fileAndDirPaths.map((fileAndDirPath) => findHtmlFiles(`${publishDir}${fileAndDirPath}`, directoryFilter)), 73 | ) 74 | 75 | return [].concat(...htmlFilePaths) as string[] 76 | } 77 | 78 | const findHtmlFiles = async function (fileAndDirPath: string, directoryFilter: '*' | string[]): Promise { 79 | if (await isDirectory(fileAndDirPath)) { 80 | const filePaths = [] 81 | const stream = readdirp(fileAndDirPath, { 82 | directoryFilter, 83 | fileFilter: GLOB_HTML, 84 | }) 85 | 86 | for await (const { path } of stream) { 87 | filePaths.push(join(fileAndDirPath, path)) 88 | } 89 | 90 | return filePaths 91 | } 92 | 93 | if (!(await isFile(fileAndDirPath))) { 94 | console.warn( 95 | `Path ${fileAndDirPath} was provided in "checkPaths", but does not exist. This could indicate a problem with your build. If you want, you can simply delete this path from your "checkPaths" key in netlify.toml`, 96 | ) 97 | return EMPTY_ARRAY 98 | } 99 | 100 | if (extname(fileAndDirPath) !== HTML_EXT) { 101 | return EMPTY_ARRAY 102 | } 103 | 104 | return [fileAndDirPath] 105 | } 106 | -------------------------------------------------------------------------------- /src/reporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This reporter is adapted from the CLI reporter built into the pa11y library, 3 | * with some small differences for performance reasons. 4 | * 5 | * @see https://github.com/pa11y/pa11y/blob/6.1.1/lib/reporters/cli.js 6 | */ 7 | 8 | 'use strict' 9 | 10 | import { bold, cyan, green, gray, red, underline, yellow } from 'picocolors' 11 | 12 | // Pa11y version support 13 | const PA11Y_SUPPORTS = '^6.0.0 || ^6.0.0-alpha || ^6.0.0-beta' 14 | 15 | const DUPLICATE_WHITESPACE_EXP = /\s+/g 16 | const EMPTY_SPACE = ' ' 17 | const NEWLINE_LITERAL = '\n' 18 | 19 | const LOCAL_FILE_PATH_EXP = new RegExp(`^http://localhost:\\d{4}/|${'file://' + process.cwd()}/`); 20 | 21 | // Helper strings for use in reporter methods 22 | const start = cyan(' >') 23 | const typeIndicators = { 24 | error: red(' • Error:'), 25 | notice: cyan(' • Notice:'), 26 | unknown: gray(' •'), 27 | warning: yellow(' • Warning:'), 28 | } 29 | 30 | function renderIssue(issue) { 31 | const code = issue.code 32 | const selector = issue.selector.replace(DUPLICATE_WHITESPACE_EXP, EMPTY_SPACE) 33 | const context = issue.context ? issue.context.replace(DUPLICATE_WHITESPACE_EXP, EMPTY_SPACE) : '[no context]' 34 | 35 | return cleanWhitespace(` 36 | 37 | ${typeIndicators[issue.type]} ${issue.message} 38 | ${gray(` ├── ${code}`)} 39 | ${gray(` ├── ${selector}`)} 40 | ${gray(` └── ${context}`)} 41 | `) 42 | } 43 | 44 | // Output formatted results 45 | function renderResults(results): string { 46 | if (results.issues.length) { 47 | const publicFilePath = results.pageUrl.replace(LOCAL_FILE_PATH_EXP, '') 48 | const totals = { 49 | error: 0, 50 | notice: 0, 51 | warning: 0, 52 | } 53 | const issues = [] 54 | const summary = [] 55 | 56 | for (const issue of results.issues) { 57 | issues.push(renderIssue(issue)) 58 | totals[issue.type] = totals[issue.type] + 1 59 | } 60 | 61 | if (totals.error > 0) { 62 | summary.push(red(`${totals.error} ${pluralize('Error', totals.error)}`)) 63 | } 64 | if (totals.warning > 0) { 65 | summary.push(yellow(`${totals.warning} ${pluralize('Warning', totals.warning)}`)) 66 | } 67 | if (totals.notice > 0) { 68 | summary.push(cyan(`${totals.notice} ${pluralize('Notice', totals.notice)}`)) 69 | } 70 | 71 | return cleanWhitespace(` 72 | 73 | ${bold(`Results for file ${underline(publicFilePath)}:`)} 74 | ${issues.join(NEWLINE_LITERAL)} 75 | 76 | ${summary.join(NEWLINE_LITERAL)} 77 | 78 | `) 79 | } 80 | return cleanWhitespace(` 81 | ${green('No issues found!')} 82 | `) 83 | } 84 | 85 | // Output the welcome message once Pa11y begins testing 86 | function renderBegin() { 87 | return cleanWhitespace(` 88 | ${cyan(underline('Welcome to Pa11y'))} 89 | `) 90 | } 91 | 92 | // Output debug messages 93 | function renderDebug(message: string) { 94 | message = `Debug: ${message}` 95 | return cleanWhitespace(` 96 | ${start} ${gray(message)} 97 | `) 98 | } 99 | 100 | // Output information messages 101 | function renderInfo(message: string) { 102 | return cleanWhitespace(` 103 | ${start} ${message} 104 | `) 105 | } 106 | 107 | function renderError(message: string) { 108 | if (!/^error:/i.test(message)) { 109 | message = `Error: ${message}` 110 | } 111 | return cleanWhitespace(` 112 | ${red(message)} 113 | `) 114 | } 115 | 116 | // Clean whitespace from output. This function is used to keep 117 | // the reporter code a little cleaner 118 | function cleanWhitespace(string): string { 119 | return string.replace(/\t+|^\t*\n|\n\t*$/g, '') 120 | } 121 | 122 | function pluralize(noun: string, count: number): string { 123 | return count === 1 ? noun : noun + 's' 124 | } 125 | 126 | 127 | 128 | export { 129 | renderBegin as begin, 130 | renderDebug as debug, 131 | renderInfo as info, 132 | renderError as error, 133 | renderResults as results, 134 | PA11Y_SUPPORTS as supports, 135 | } 136 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import http, { Server } from 'http' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import MIME_TYPES from './mimeTypes.json' 5 | 6 | const HTML_EXT = '.html' 7 | 8 | const SERVER_HOST = 'localhost' 9 | const SERVER_PORT = '9000' 10 | const SERVER_ADDRESS = 'localhost:' + SERVER_PORT 11 | 12 | const SERVER_OPTS = { 13 | host: SERVER_HOST, 14 | port: SERVER_PORT, 15 | } 16 | 17 | const basePath = process.cwd() 18 | 19 | class StaticServer { 20 | instance: Server 21 | constructor(publishDir: string) { 22 | this.instance = http.createServer(function (req, res) { 23 | const ext = path.extname(req.url) 24 | const filepath = ext === HTML_EXT ? path.join(basePath, req.url) : path.join(basePath, publishDir, req.url) 25 | 26 | res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'text/plain' }) 27 | 28 | const stream = fs.createReadStream(filepath) 29 | 30 | stream 31 | .on('open', function () { 32 | stream.pipe(res) 33 | }) 34 | .on('error', function (err) { 35 | res.statusCode = 500 36 | res.end(`Error getting the file: ${err}.`) 37 | }) 38 | 39 | res.on('close', function () { 40 | stream.destroy() 41 | }) 42 | }) 43 | } 44 | 45 | listen() { 46 | return this.instance.listen(SERVER_OPTS) 47 | } 48 | } 49 | 50 | export { 51 | StaticServer, 52 | SERVER_ADDRESS, 53 | } 54 | -------------------------------------------------------------------------------- /tests/generateFilePaths/__snapshots__/this.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generateFilePaths works 1`] = ` 4 | Array [ 5 | "tests/generateFilePaths/publishDir/blog/post1.html", 6 | "tests/generateFilePaths/publishDir/blog/post2.html", 7 | "tests/generateFilePaths/publishDir/about.html", 8 | ] 9 | `; 10 | -------------------------------------------------------------------------------- /tests/generateFilePaths/publishDir/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/generateFilePaths/publishDir/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/generateFilePaths/publishDir/blog/post1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/generateFilePaths/publishDir/blog/post2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/generateFilePaths/publishDir/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

intentionally inaccessible img with no alt

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/generateFilePaths/this.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const PUBLISH_DIR = path.join(__dirname, 'publishDir') 4 | const publishDir = path.relative(process.cwd(), PUBLISH_DIR) 5 | // actual test 6 | 7 | const pluginCore = require('../../src/pluginCore') 8 | test('generateFilePaths works', async () => { 9 | const results = await pluginCore.generateFilePaths({ 10 | publishDir: publishDir, 11 | fileAndDirPaths: ['/blog', '/about.html'], 12 | ignoreDirectories: [], 13 | }) 14 | expect(results).toMatchSnapshot() 15 | }) 16 | 17 | const pathInResults = (expectedPath, results) => { 18 | return results.findIndex((r) => r.endsWith(expectedPath)) != -1 19 | } 20 | 21 | test('ignoreDirectories works including leading slash', async () => { 22 | const results = await pluginCore.generateFilePaths({ 23 | fileAndDirPaths: ['/'], 24 | ignoreDirectories: ['/admin'], 25 | publishDir: publishDir, 26 | }) 27 | expect(pathInResults('publishDir/blog/post1.html', results)).toBe(true) 28 | expect(pathInResults('publishDir/about.html', results)).toBe(true) 29 | expect(pathInResults('publishDir/index.html', results)).toBe(true) 30 | expect(pathInResults('publishDir/admin/index.html', results)).toBe(false) 31 | }) 32 | 33 | test('ignoreDirectories works without leading slash', async () => { 34 | const results = await pluginCore.generateFilePaths({ 35 | fileAndDirPaths: ['/'], 36 | ignoreDirectories: ['admin'], 37 | publishDir: publishDir, 38 | }) 39 | expect(pathInResults('publishDir/blog/post1.html', results)).toBe(true) 40 | expect(pathInResults('publishDir/about.html', results)).toBe(true) 41 | expect(pathInResults('publishDir/index.html', results)).toBe(true) 42 | expect(pathInResults('publishDir/admin/index.html', results)).toBe(false) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/runPa11y/__snapshots__/this.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`runPa11y works 1`] = ` 4 | Object { 5 | "issueCount": 1, 6 | "report": " 7 | Results for file tests/runPa11y/publishDir/index.html: 8 | 9 |  • Error: Images must have alternate text (https://dequeuniversity.com/rules/axe/4.2/image-alt?application=axeAPI) 10 |  ├── image-alt 11 |  ├── html > body > img 12 |  └──  13 | 14 | 1 Error 15 | ", 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /tests/runPa11y/publishDir/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

intentionally inaccessible img with no alt

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/runPa11y/this.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { getPa11yOpts } = require('../../src/config') 3 | const filePath = path.relative(process.cwd(), path.join(__dirname, 'publishDir/index.html')) 4 | 5 | // actual test 6 | const pluginCore = require('../../src/pluginCore') 7 | test('runPa11y works', async () => { 8 | const results = await pluginCore.runPa11y({ 9 | build: { failBuild() {} }, 10 | htmlFilePaths: [filePath], 11 | pa11yOpts: await getPa11yOpts({}), 12 | }) 13 | expect(results).toMatchSnapshot() 14 | }) 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "lib": ["ES2019"], 6 | "outDir": "./lib", 7 | "types": ["jest"], 8 | "esModuleInterop": true, 9 | "removeComments": true, 10 | "importHelpers": true, 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true /* Skip type checking of declaration files. */, 13 | "forceConsistentCasingInFileNames": true, 14 | }, 15 | "include": ["src/**/*.ts"] 16 | } 17 | --------------------------------------------------------------------------------