Skip to content

How to Check for Broken Links in Your CI/CD Pipeline

20 October 2026 · 7 min read

Most broken links don't come from editors deleting pages. They come from deploys: a route gets renamed, an image doesn't make it into the build, a redirect rule changes, a trailing slash behaves differently on the new server. Your tests pass, the deploy succeeds, and the site is quietly worse than it was an hour ago.

The fix is to treat links like any other regression: check them automatically after every deploy, and fail the pipeline when something breaks. This guide shows how to do that with the free 4F API and its command-line client - for GitHub Actions, GitLab CI, or any pipeline that can run a shell command.

How it works

The 4F CLI starts a scan through the REST API, waits for it to finish, prints the results and exits with a code your pipeline understands:

  • -0 - no issues found
  • -1 - issues found
  • -2 - something went wrong (invalid API key, scan failed, timeout)

The crawl itself runs on 4F's servers, not in your CI runner. The runner only needs Node.js 18 or newer to run the CLI, and it spends most of the time waiting.

1. Create an API key and a project

Sign in, open Account → API keys and create a key. API access is included in the free plan. The key is shown only once, so store it straight away as a secret in your CI system, for example as FOUREFF_API_KEY. The CLI reads it from that environment variable.

Then create a project for the site you deploy - either in the dashboard or with the CLI:

shell
export FOUREFF_API_KEY=4f_your_key_here
npx @4f/cli projects create --name "My site" --url https://example.com

The output contains the project ID. Store it as a CI variable too, for example FOUREFF_PROJECT_ID.

Why a project, not just a URL? 4f scan https://example.com works too, but it creates a new project on every run. The free plan allows 2 projects, so a pipeline would hit the limit on its third run. Scanning an existing project with --projectavoids that, keeps a scan history you can compare in the dashboard, and applies the project's settings such as ignore patterns.

2. Add the check to your pipeline

GitHub Actions

Add a job that runs after your deploy job. GitHub's Ubuntu runners come with Node.js, so no setup step is needed:

.github/workflows/deploy.yml
jobs:
  deploy:
    # ... your existing deploy job

  link-check:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - name: Check for broken links
        env:
          FOUREFF_API_KEY: ${{ secrets.FOUREFF_API_KEY }}
        run: npx --yes @4f/cli scan --project ${{ vars.FOUREFF_PROJECT_ID }} --skip-external --skip-seo --format text

GitLab CI

.gitlab-ci.yml
link-check:
  stage: .post
  image: node:20
  script:
    - npx --yes @4f/cli scan --project "$FOUREFF_PROJECT_ID" --skip-external --skip-seo --format text

Add FOUREFF_API_KEY as a masked CI/CD variable in the project settings.

Any other pipeline

Gitea Actions, Bitbucket Pipelines, Jenkins, a deploy script - it's the same command. If npx isn't practical, install the CLI once with npm install -g @4f/cli and call 4f scan ....

3. Choose what should fail the build

By default, any finding makes the scan exit with 1. For a deploy gate you usually want to be stricter about your own site than about the rest of the internet:

  • ---skip-external ignores links to other websites. A third-party page going offline shouldn't block your release - check those in a scheduled scan instead. Your own pages and assets (images, scripts, stylesheets) are still checked.
  • ---skip-seo leaves out page-quality findings like long titles or missing meta descriptions. Useful to know, rarely a reason to stop a deploy.
  • ---format text prints a readable summary in the job log. The default is JSON, which is better if you want to process the results.

Even with both flags, minor findings such as a slow response still count. If only errors - broken internal links, missing assets, SSL problems - should fail the build, write the JSON report to a file and decide with jq:

shell
# Exit code 1 (issues found) is fine here - we decide below. 2 still fails.
npx --yes @4f/cli scan --project "$FOUREFF_PROJECT_ID" --skip-external --quiet > 4f-report.json || [ $? -eq 1 ]

# Fail only if at least one issue has severity "error"
jq -e '[.results[].issues[] | select(.severity == "error")] | length == 0' 4f-report.json

The report lists every affected URL with its status code, the page that links to it and the issue details - worth keeping as a build artifact.

4. Know the limits

A few things to plan for before you put this in front of every deploy:

  • -Scans use your page quota. Every scan crawls the whole site and counts against the monthly quota - 10,000 pages on the free plan, with up to 3,000 pages per scan. A 500-page site deployed a few times a day adds up. If you deploy often, scan only on releases or on the main branch.
  • -Larger sites take a while. The crawler respects a per-site request rate (5 requests per second by default), so a few thousand URLs including assets and external links can take several minutes. The CLI waits up to 10 minutes; raise it with --timeout (in seconds). Add --cancel-on-timeout if a timed-out scan should be stopped on the server too.
  • -No JavaScript rendering. 4F reads the HTML your server returns. Server-rendered sites and frameworks with SSR or static export work well; links that only appear after client-side JavaScript runs are not discovered.
  • -Bot protection can get in the way. If your CDN or firewall challenges automated traffic, allowlist the User-Agent prefix 4f.at/crawler. Otherwise pages the crawler can't reach show up as blocked, and the scan can't check them.
  • -The site has to be reachable. Scans run from the internet, so preview deployments behind a login or VPN can't be checked. Scan the public URL after it's live, or a public staging host.

Don't block the deploy at all?

Sometimes you'd rather know than stop. Run the step with continue-on-error: true (GitHub Actions) or allow_failure: true (GitLab), and the pipeline shows a warning instead of failing. Combine it with a scheduled scan in the dashboard (Starter and up) to also catch external links that break between deploys.

Prefer to call the API directly? The CLI is a thin wrapper around three requests: start a scan (POST /api/v1/projects/{id}/scans), poll its status (GET /api/v1/scans/{id}) and fetch the results (GET /api/v1/scans/{id}/results). See the API docs or import the OpenAPI specification into your tool of choice.

Try it now - it's free, no account needed

Up to 1,000 pages. Results in minutes.