• Fixing my blog (part 6) - Accessibility scanning

    Now we’ve got broken links sorted, we’re in a better state to start accessibility testing using the Accessibility Insights Action. This is available as both a GitHub Action and an Azure DevOps extension. I’ll be using the GitHub Action version.

    
    name: Accessibility
    
    on:
      workflow_dispatch:
        
    jobs:
      build:
        runs-on: ubuntu-latest
    
        steps:
          - uses: actions/checkout@v2
    
          - uses: ./.github/actions/jekyll-build-pages
            with:
              verbose: false
    
          - run: |
              sudo find -type f ! -regex ".*\.\(html\|svg\|gif\|css\|jpg\|png\)" -delete
            name: Remove non-HTML
            
            # https://github.com/microsoft/accessibility-insights-action
          - name: Scan for accessibility issues
            uses: microsoft/accessibility-insights-action@v2
            with:
                repo-token: ${{ secrets.GITHUB_TOKEN }}
                site-dir: ${{ github.workspace }}/_site
                scan-timeout: 6000000
                #max-urls: 1500
                localhost-port: 12345
                scan-url-relative-path: /
    
          - name: Upload report artifact
            uses: actions/upload-artifact@v2
            with:
                name: accessibility-reports
                path: ${{ github.workspace }}/_accessibility-reports/index.html
    
    

    The action can either scan a local directory or a URL. In my case, I want it to scan all the files that make up my blog. My blog content is written in Markdown (.md) files and uses the Jekyll engine to render those pages into .html. It’s the latter that should be scanned for accessibility compliance.

    To generate the .html files, I make use of the Jekyll-Build-Pages action. This will generate a bunch of files under the _site directory.

    My initial testing with the scanning tool revealed it was triggering on some pages I was including but had no control over (eg. Google Ads and the Disqus comments). I wanted to exclude those from the scanning, and one way to do that is to make the content conditional. eg.

    
    {%- if jekyll.environment == "production" -%}
    <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-999999999" crossorigin="anonymous"></script>
    {%- endif -%}
    
    

    To exclude this block, I then needed to ensure that jekyll.environment was not set to production. I achieved this by using a local copy of the action in which I set the value of the JEKYLL_ENV environment variable to development. To facilitate that I need to make a few other changes which you can see on this branch.

    To help focus the scanning just on the files I care about, I added an extra step to the workflow to remove any files that weren’t one of .html, .svg, .gif, .jpg or .png.

    Depending on how many files you have in your website, scanning can take quite.

    It’s probably a good idea to not set max-urls the first time. This uses the default of 100, which will be enough to give you an idea of the kinds of problems you need to fix.

    The reason for starting small is if you have a template or CSS that are used across every page, then every page will trigger errors, and your scan will take ages to finish and contains heaps of the same error(s).

    Once you’ve resolved those common errors, then you can ramp up your max-urls to cover all the pages on your site (if it didn’t already).

    Running the workflow above also produces an ‘Accessibility Checks’ report. You can use this to get a quick overview of the results. To drill in to the details, you should download the build artifact view the index.html file in your browser.

    Accessibility report screenshot

    Fixing the errors

    The scan flagged 5 rule violations. Expanding each rule lists the URLs that exhibited the problem, plus a suggestion on how to resolve the issue. Sometime there are multiple suggestions.

    color-contrast: Ensure the contrast between foreground and background colors meet WCAG 2 AA contrast ration thresholds

    Example

    <a href="/tag/Family.html">Family</a>
    

    Fix the following:

    • Element has insufficient color contrast of 4.14 (foreground color: #2a7ae2, background color: #fdfdfd, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1Element has insufficient color contrast of 4.14 (foreground color: #2a7ae2, background color: #fdfdfd, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1

      • Use foreground color: #2773d6 and the original background color: #fdfdfd to meet a contrast ratio of 4.58:1.
    • Element has insufficient color contrast of 3.77 (foreground color: #828282, background color: #fdfdfd, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1Element has insufficient color contrast of 3.77 (foreground color: #828282, background color: #fdfdfd, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1

      • Use foreground color: #747474 and the original background color: #fdfdfd to meet a contrast ratio of 4.59:1.

    These were common across all pages, so fixing these early is a quick win. I searched for the hex color (usually it was defined in one of the .scss files that generate the CSS) and replaced it with the suggested colour

    Fix the following:

    • Element is in tab order and does not have accessible text

    Fix ONE of the following:

    • Element does not have text that is visible to screen readers
    • aria-label attribute does not exist or is empty
    • aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
    • Element has no title attribute

    image-alt: Ensures <img> elements have alternate text or a role of none or presentation

    Fix ONE of the following:

    • Element does not have an alt attribute
    • aria-label attribute does not exist or is empty
    • aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
    • Element has no title attribute
    • Element’s default semantics were not overridden with role=”none” or role=”presentation”

    html-has-lang: Ensure every HTML document has a lang attribute

    Fix the following:

    • The <html> element does not have a lang attribute

    This was a false positive as for some reason it was being flagged on images.

    frame-title: Ensures <iframe> and <frame> elements have an accessible name

    Fix ONE of the following:

    • Element has no title attribute
    • aria-label attribute does not exist or is empty
    • aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
    • Element’s default semantics were not overridden with role=”none” or role=”presentation”

    This was being flagged by some older YouTube embedded player HTML.

    I’ve raise a bug to report the problem with links to images being flagged. It feels to me like the scanner is trying to scan image URLs (which it shouldn’t). Whether that’s a bug in the action, or in how I’m using it, I hope to find out soon.

  • Fixing my blog (part 5) - Putting it all together with a PR

    In part 4 of this series of posts we used the Replace multiple strings in files GitHub Action to update files with the replacement values. The files are modified on disk, but what to do with the changes? I could just automatically commit the changes back to version control, but I prefer to take a more cautious approach and give myself a chance to review the changes to confirm if they look reasonable. Creating a pull request is a great way of doing that.

    A an action I’ve used successfully before to do this is Peter Evans’ Create Pull Request GitHub Action. Using it is very easy:

    
    - name: Create Pull Request
      uses: peter-evans/[email protected]
      with:
        token: ${{ secrets.PAT_REPO_FULL }}
    
    

    I pass in a personal access token so that any workflows that should be run on creation of a pull request will be executed.

    And with that, I have a full workflow for checking for broken links and repairing them. The only thing to watch out for is that issue I mentioned previously with the invalid JSON produced by the Lychee action. As a temporary measure, I inlined that action in my repo and applied a local code fix. Hopefully once the Lychee action itself is updated then I can go back to using their implementation.

    
    name: Links
    
    on:
      workflow_dispatch:
        
    jobs:
      linkChecker:
        runs-on: ubuntu-latest
    
        steps:
          - uses: actions/checkout@v3
          - name: Link Checker
            id: lychee
            uses: ./.github/actions/lychee-action #lycheeverse/[email protected]
            env:
              GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
            with:
              args: '--verbose ./_posts/**/*.md --exclude-mail --scheme http https'
              format: json
              output: ./lychee/links.json
              fail: false
    
          - uses: actions/[email protected]
            with:
              path: ./lychee/links.json
    
          - name: Wayback Machine Query
            uses: flcdrg/wayback-machine-query-action@v2
            id: wayback
            with:
              source-path: ./lychee/links.json
              timestamp-regex: '_posts\/(\d+)\/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)-'
    
          - uses: actions/[email protected]
            with:
              path: ./wayback/replacements.json
              
          - name: Replacements
            uses: flcdrg/replace-multiple-action@v1
            with:
              find: ${{ steps.wayback.outputs.replacements }}
              prefix: '(^|\\s+|\\()'
              suffix: '($|\\s+|\\))'
              
          - name: Create Pull Request
            # if: ${{ github.ref == 'refs/heads/main' }}
            uses: peter-evans/[email protected]
            with:
              token: ${{ secrets.PAT_REPO_FULL }}
    
    

    Now that we’ve addressed the broken links, we’re in a better state to revisit the Accessibility Insights Action that started this whole adventure!

  • Fixing my blog (part 4) - Updating the files

    Last time we looked at using the Wayback Machine Query GitHub Action to automate querying the Wayback Machine for all our broken links. Now we need to apply the changes to our files.

    Ideally there’d be a GitHub Action that could take a list of our changes and apply them to a list of files. I did search for something like that but all I could find were actions that only made one single change. Time to create another action.

    Enter the Replace multiple strings in files GitHub Action.

    For example, to find all instances of ‘Multiple’ and replace them with ‘Many’ for all the .md files in the current directory you can do:

    - uses: flcdrg/replace-multiple-action@v1
      with:
        files: './*.md'
        find: '[{ "find": "Multiple", "replace": "Many" }]'
    

    For my case I want something like this:

    - uses: flcdrg/replace-multiple-action@v1
      with:
        files: './*.md'
        find: '[{ "find": "http://localhost", "replace": "https://localhost"}, { "find": "http://davidgardiner.net.au", "replace": "https://david.gardiner.net.au" }]'
        prefix: '(^|\\s+|\\()'
        suffix: '($|\\s+|\\))'
    

    The prefix and suffix input properties need some explanation. Originally I was just using a plain string find and replace, but I discovered that there was a problem.

    Consider that I could have multiple broken links to a site. eg. blog.spencen.com/2010/09/04/word-puzzle-to-sliverlight-phonendashpart-3.aspx and blog.spencen.com.

    I replace all the instances of the first URL with http://web.archive.org/web/20100926212957/http://blog.spencen.com/2010/09/04/word-puzzle-to-sliverlight-phonendashpart-3.aspx.

    The problem comes with the next find/replace in that it is now looking for blog.spencen.com and that value also exists in the new snapshot URL! We potentially end up in a recursive ‘inception’ mess. To avoid this, we can supply some partial regular expressions that get concatenation before and after the broken URL when we’re searching. In my case those expressions mean that blog.spencen.com won’t be matched in the middle of the snapshot URL.

    I’ve made these properties so the action is more general than just my use case of updating my blog posts.

    To actually use the action in concert with the previous actions, I’m using it thus:

    
    - name: Replacements
      uses: flcdrg/replace-multiple-action@v1
      with:
        find: ${{ steps.wayback.outputs.replacements }}
        prefix: '(^|\\s+|\\()'
        suffix: '($|\\s+|\\))'
    
    

    The key difference here is that I’m making use of the output property from the Wayback Machine Query action.

    Our files have been updated. Next we finish off the workflow by creating a pull request with the changes.