{"id":26246030,"url":"https://github.com/zillow/seolint","last_synced_at":"2025-04-23T20:26:19.695Z","repository":{"id":46946500,"uuid":"117125278","full_name":"zillow/seolint","owner":"zillow","description":"A node based SEO linting tool","archived":false,"fork":false,"pushed_at":"2022-12-07T17:42:08.000Z","size":434,"stargazers_count":62,"open_issues_count":11,"forks_count":3,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-03-30T03:11:47.713Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zillow.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-01-11T16:37:35.000Z","updated_at":"2025-03-16T22:25:17.000Z","dependencies_parsed_at":"2023-01-24T19:33:43.351Z","dependency_job_id":null,"html_url":"https://github.com/zillow/seolint","commit_stats":null,"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fseolint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fseolint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fseolint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zillow%2Fseolint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zillow","download_url":"https://codeload.github.com/zillow/seolint/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250507946,"owners_count":21442127,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-13T13:17:15.867Z","updated_at":"2025-04-23T20:26:19.667Z","avatar_url":"https://github.com/zillow.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SEOLint\n\n[![npm version](https://badge.fury.io/js/%40zillowgroup%2Fseolint.svg)](https://badge.fury.io/js/%40zillowgroup%2Fseolint)\n\nSEOLint is a linting tool for validating SEO best practices on your web pages.\nFor each URL given, the tool does a server-side and client-side render (using [request](https://github.com/request/request) and [puppeteer](https://github.com/GoogleChrome/puppeteer) respectively) and runs the content against a library of SEO rules.\nDepending on your application, your server and client content can vary dramatically -- while search engines are very competent at crawling dynamic content, there are still some small \"gotchas\" that SEOLint helps you identify.\n\n## Installation \u0026 Upgrading\n\nSEOLint requires node version 8.3.0 or greater. We highly recommend [nvm](https://github.com/creationix/nvm) for installing node.\nOnce you have node/npm, you can install/upgrade SEOLint globally with the following command:\n\n```bash\nnpm install -g @zillowgroup/seolint@latest\n```\n\n## Usage\n\nTo run SEOLint on a single url:\n\n```bash\n@zillowgroup/seolint https://www.zillow.com/\n```\n\nTo run with a [configuration file](https://github.com/zillow/seolint#seolintconfigjs):\n\n```bash\n@zillowgroup/seolint --config seolint.config.js\n```\n\nTo see the full usage information:\n\n```bash\n@zillowgroup/seolint --help\n```\n\n## SEO Rules\n\nBelow are the rules run for every url you give to SEOLint. These are general recommendations that you may want to configure, override, or disable.\n\n#### H1Tag (`\"error\"`)\n\nVerifies that the page has one and only one `\u003ch1\u003e` tag.\n\n#### TitleTag (`\"error\"`)\n\nVerifies that the page has a `\u003ctitle\u003e` tag with an appropriate length (no more than 60 characters).\n\n* https://moz.com/learn/seo/title-tag\n\n#### MetaDescription (`\"error\"`)\n\nVerifies that the page has a `\u003cmeta name=\"description\" content=\"\" /\u003e` tag with an appropriate length (between 50-300 characters).\n\n* https://moz.com/learn/seo/meta-description\n\n#### ImageAltAttribute (`\"error\"`)\n\nVerifies that all `\u003cimg\u003e` tags have an alt text attribute.\nDecorative images that don't add information to the content of the page should have an empty alt attribute (`alt=\"\"`) so they can be ignored by screen readers.\n\n* https://moz.com/learn/seo/alt-text\n* https://www.w3.org/WAI/tutorials/images/decorative/\n\n#### NoRedirect (`\"error\"`)\n\nVerifies that the page was not redirected. You can customize the validator to alternatively verify that the page _was_ redirected.\n\n#### MixedContent (`\"error\"`)\n\nVerifies that the page has no mixed-content resources.\n\n* https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content\n\n#### ConsistentTrailingSlash (`\"warn\"`)\n\nVerifies that all the links on your page use consistent trailing slashes.\n\n* https://webmasters.googleblog.com/2010/04/to-slash-or-not-to-slash.html\n\nNote: Inconsistent trailing slashes are not necessarily a bad thing on their own,\nyou just have to make sure that your redirects are set up correctly and you are linking to the correct version.\nUltimately we want to prevent duplicate content and unnecessary redirects.\n\n##### `options`\n\n* `noSlash` (`boolean`): By default, the rule verifies that all links have a trailing slash.\nSet this to true to verify that all links do not have a trailing slash.\n\n#### Canonical (`\"error\"`)\n\nVerifies that the page has one canonical link with a fully resolved url.\n\n* https://webmasters.googleblog.com/2013/04/5-common-mistakes-with-relcanonical.html\n\n##### `options`\n\n* `expectedPath` (`string`): By default, the rule verifies that the canonical matches the page url.\nSet this to specify a different expected canonical path.\n\n## seolint.config.js\n\nSEOLint supports JavaScript and JSON configuration files - you can see an example of each in the [examples folder](https://github.com/zillow/seolint/tree/master/examples).\n\n```javascript\nconst path = require('path');\n\nmodule.exports = {\n    // {array} url configurations\n    urls: [\n        // {string} url with default configuration\n        'https://www.zillow.com/',\n\n        // {object} custom url configuration\n        {\n            // {string} url\n            url: 'https://www.zillow.com/mortgage-rates/',\n\n            // {object} url rule configurations\n            rules: {\n                TitleTag: {\n                    // {number | string} reporting level for this rule\n                    level: 'warn',\n\n                    // {object} rule specific options\n                    options: {}\n                }\n            }\n        }\n    ],\n\n    // {string} force all urls to use this hostname\n    hostname: 'https://www.zillow.com/',\n\n    // {string} directory of custom rules to include\n    rulesdir: path.join(__dirname, 'path/to/my/rules'),\n\n    // {object} global rule configurations\n    rules: {\n        TitleTag: 'off'\n    }\n};\n```\n\n## Configuring Rules\n\nYou can change the severity of rules by changing the rule level in your configuration. Most rules run with an `\"error\"` level by default. If any rule fails at the `\"error\"` level, the program will finish with an exit code of 1. Alternatively, you can run rules at the `\"warn\"` level which will not trigger an exit code of 1. To turn a rule off completely, set the level to `\"off\"`.\n\nYou can also use numeric levels `2`, `1`, and `0`, for `\"error\"`, `\"warn\"`, and `\"off\"` respectively.\n\n### Global Configuration\n\nTo change the level of a rule for all urls, add the configuration at the root of your configuration file:\n\n```javascript\n{\n    rules: {\n          TitleTag: \"warn\",\n          Canonical: \"off\",\n          MixedContent: \"error\"\n    }\n}\n```\n\n### URL Specific Configuration\n\nYou can override global configurations for any given url by adding a rules object to your url configuration:\n\n```javascript\n{\n    urls: [\n        {\n            url: 'https://www.zillow.com/',\n            rules: {\n                TitleTag: \"warn\",\n                Canonical: \"off\",\n                MixedContent: \"error\"\n            }\n        }\n    ]\n}\n```\n\n## Custom Rules\n\nYou can write your own rules and include them when running tests.\nCustom rules follow the same format as the default rules with a `parser` and `validator` function (see the [default rules](https://github.com/zillow/seolint/tree/master/src/rules) or [examples](https://github.com/zillow/seolint/tree/master/examples/rules) for inspiration). To include your rules, specify `rulesdir` in your [configuration file](https://github.com/zillow/seolint#seolintconfigjs) or `--rulesdir` on the command line:\n\n```bash\n@zillowgroup/seolint --rulesdir path/to/my/rules https://www.zillow.com/\n```\n\n## Advanced Configuration\n\nIn some cases, you will want to override the default behavior of rules in your configuration file.\nEach rule consists of a parser and a validator function.\nAfter SEOLint renders your page, it passes all the render data to the parser function,\nthe result of which is passed to the validator. Both parser and validator are given options as the second parameter.\n\nSEO rules were broken up into separate parsers and validators so that you can tweak validation conditions without having to re-parse the render data.\nYou can override the parser, the validator, or both, but be mindful when changing the parser as it will also change the input to the validator.\n\n### `parser(data, options)`\n\nBelow is the structure of parser `data`:\n\n```javascript\n{\n    // {string} The url of the string being tested\n    url: '',\n\n    // {object} Data from the client render\n    client: {\n        // {string} The HTML content rendered by the client\n        content: '',\n\n        // {object} Resource data for each requested resource\n        resources: {\n            'resource url': {\n                // {object} The requestData returned from puppeteer's \"request\" page event\n                request: {},\n\n                // {object} The response returned from puppeteers's \"response\" page event\n                response: {}\n            }\n        }\n    },\n\n    // {object} Data from the server render\n    server: {\n        // {string} The HTML content rendered by the server\n        content: '',\n\n        // {object} The response object from the request API\n        response: {}\n    }\n}\n```\n\n### `validator(parsed, options)`\n\nValidators are simple functions that take the output of the parser function as input. If the validator runs without throwing an error, the test is successful. If you want the validator to fail, just throw an error. The default validators use the [chai assertion library](http://chaijs.com/api/bdd/) for validating the parsed page data.\n\n### seolint.config.js\n\n`parser` and `validator` overrides should go on the rules object alongside the `level` and `options` keys.\nJust like the latter, the overrides can be global or specific to a URL.\n\nNote: If you are using a JavaScript configuration file that has third-party module dependencies (e.g. chai), make sure to install those dependencies at the location of your config file, otherwise seolint will fail. It's a good idea to `npm i --save-dev` those dependencies if your seolint config file lives alongside your `package.json`.\n\n```javascript\nconst expect = require('chai').expect;\nconst path = require('path');\n\nmodule.exports = {\n    urls: [{\n        url: 'https://www.zillow.com/mortgage-rates/',\n        rules: {\n            TitleTag: {\n                // {function} locally override the default parser\n                parser: (url, clientPage, serverPage) =\u003e ({ myClientTitle: 'foo', myServerTitle: 'foo' })\n            }\n        }\n    ],\n    rules: {\n        TitleTag: {\n            // {function} globally override the default validator\n            validator: ({ myClientTitle, myServerTitle }) =\u003e { expect(myClientTitle).to.equal(myServerTitle); }\n        }\n    }\n};\n```\n\n### Default Parsing Functions\n\nIn the event that you want to override the validator only, the following default values will be passed to your validator:\n\n#### `H1TagParser =\u003e { clientH1s, serverH1s }`\n\n* `clientH1s` (`array`): Array of h1 text strings found on the client rendering\n* `serverH1s` (`array`): Array of h1 text strings found on the server rendering\n\n#### `TitleTagParser =\u003e { clientTitle, serverTitle }`\n\n* `clientTitle` (`string`): The client rendered title text\n* `serverTitle` (`string`): The server rendered title text\n\n#### `MetaDescriptionParser =\u003e { clientDescription, serverDescription }`\n\n* `clientDescription` (`string`): The client rendered description content\n* `serverDescription` (`string`): The server rendered description content\n\n#### `ImageAltAttributeParser =\u003e { clientImages, serverImages }`\n\n* `clientImages` (`array`): Array of client rendered image objects including a `src` and `alt` property\n* `serverImages` (`array`): Array of server rendered image objects including a `src` and `alt` property\n\n#### `NoRedirectParser =\u003e { referer, href }`\n\n* `referer` (`string`): The URL of the referring page that initiated the redirect.\n* `href` (`string`): The URL of the resulting page after the redirect.\n\n#### `MixedContentParser =\u003e { isSecure, insecureResources }`\n\n* `isSecure` (`boolean`): Is the requested URL a secure page.\n* `insecureResources` (`array`): An array of insecure URLs requested by the page.\n\n#### `ConsistentTrailingSlashParser =\u003e { hrefsWithoutSlash, hrefs }`\n\n* `hrefsWithoutSlash` (`array`): An array of all hrefs from the same domain that do not have a trailing slash.\n* `href` (`array`): An array of all hrefs found on the page.\n\n#### `CanonicalParser =\u003e { url, clientCanonicalsHead, clientCanonicalsBody, serverCanonicalsHead, serverCanonicalsBody }`\n\n* `url` (`string`): The url of final page (after any redirects).\n* `clientCanonicalsHead` (`array`): An array of canonical links in the head of the client.\n* `clientCanonicalsBody` (`array`): An array of canonical links in the body of the client.\n* `serverCanonicalsHead` (`array`): An array of canonical links in the head of the server.\n* `serverCanonicalsBody` (`array`): An array of canonical links in the body of the server.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzillow%2Fseolint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzillow%2Fseolint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzillow%2Fseolint/lists"}