commit d57ad3573d70ea2e9deb2ad4a5c790a7606d6c1b Author: Simon C Date: Thu Apr 8 00:27:21 2021 +0200 First commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dfb333d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[js] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8494aa2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Simon Constans + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3193e94 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Website Checker + +weck permet de vérifier des bonnes pratiques pour un site internet. + +## Feature + +- [x] Scanner un dossier local +- [ ] Scanner un site distant +- [~] Permettre facilement l'extension de règle + +## Rules + +Les règles que weck vérifie : +- [x] Que la balises comporte entre 30 et 70 caractères +- [x] Vérifier qu'il n'existe qu'un seul et même titre sur un site internet diff --git a/bin/weck.js b/bin/weck.js new file mode 100644 index 0000000..84a02de --- /dev/null +++ b/bin/weck.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +import * as cli from '../src/index.js' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ba41954 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,303 @@ +{ + "name": "weck", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "weck", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "node-html-parser": "^3.1.3" + }, + "bin": { + "weck": "bin/weck.js" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "node_modules/css-select": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz", + "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^4.0.0", + "domhandler": "^4.0.0", + "domutils": "^2.4.3", + "nth-check": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz", + "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/dom-serializer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz", + "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz", + "integrity": "sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.1.tgz", + "integrity": "sha512-hO1XwHMGAthA/1KL7c83oip/6UWo3FlUNIuWiWKltoiQ5oCOiqths8KknvY2jpOohUoUgnwa/+Rm7UpwpSbY/Q==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.1.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/node-html-parser": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-3.1.3.tgz", + "integrity": "sha512-pCE2I5UY5iOBnWdJQkbYZSk+fyq2zepw0nsELpHQjVFyCzOeZhkMhnvKqGceKgzWsWx7EG4KtMqsy9Eklf5Thw==", + "dependencies": { + "css-select": "^3.1.2", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + } + }, + "dependencies": { + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "css-select": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz", + "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^4.0.0", + "domhandler": "^4.0.0", + "domutils": "^2.4.3", + "nth-check": "^2.0.0" + } + }, + "css-what": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz", + "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==" + }, + "dom-serializer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz", + "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domhandler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz", + "integrity": "sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.1.tgz", + "integrity": "sha512-hO1XwHMGAthA/1KL7c83oip/6UWo3FlUNIuWiWKltoiQ5oCOiqths8KknvY2jpOohUoUgnwa/+Rm7UpwpSbY/Q==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.1.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "node-html-parser": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-3.1.3.tgz", + "integrity": "sha512-pCE2I5UY5iOBnWdJQkbYZSk+fyq2zepw0nsELpHQjVFyCzOeZhkMhnvKqGceKgzWsWx7EG4KtMqsy9Eklf5Thw==", + "requires": { + "css-select": "^3.1.2", + "he": "1.2.0" + } + }, + "nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "requires": { + "boolbase": "^1.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..10a61c0 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "weck", + "version": "1.0.0", + "description": "Website Checker", + "main": "src/index.js", + "bin": { + "weck": "bin/weck.js" + }, + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.weko.io/weko/WebsiteChecker.git" + }, + "author": "Simon CONSTANS", + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "node-html-parser": "^3.1.3" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ec9ce87 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +bs4==0.0.1 diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..3d8be4c --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,28 @@ +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import path, { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export const getFilenames = async (directory, files = []) => { + const dirs = fs.opendirSync(directory); + for await (const dirent of dirs) { + const dirOrFilePath = path.join(directory, dirent.name); + if (dirent.isDirectory()) { + files = await getFilenames(dirOrFilePath, files) + } else if (dirent.isFile()) { + files = [...files, ...[dirOrFilePath]] + } + } + + return files +} + +export const extensionFilter = (filenames, extension) => { + return filenames.filter(filename => filename.match(new RegExp(`.*\.(${extension})`, 'ig'))); +} + +export const getContent = (filename) => { + return fs.readFileSync(filename).toString() +} diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..591e7bb --- /dev/null +++ b/src/index.js @@ -0,0 +1,45 @@ +import getRules from './rules/index.js' +import { getFilenames } from './helpers.js' + +const checkDirectory = async (directory) => { + const filenames = await getFilenames(directory); + + const rules = getRules(filenames, { directory: directory }); + let issues = [] + for (const rule of rules) { + const issue = rule.check() + if (issue) { + issues = [...issues, ...[issue]] + } + } + + createReport(issues) +} + +const createReport = (issues) => { + if (!!process.argv.find(p => p == '--md')) { + for (const rule of issues) { + if (rule.issueCount > 0) { + console.log() + console.log(`### ${rule.issueTitle}`) + console.log() + console.log(`${rule.issueDescription}`) + console.log() + for (const issue of rule.issues) { + console.log(`- element \`${issue.title}\` on \`${issue.url}\` file.`) + } + console.log() + let referenceLinks = '' + for (const reference of rule.issueReferences) { + referenceLinks += `[${reference}](${reference}), ` + } + console.log(`References : ${referenceLinks}`) + } + } + } else { + console.log(JSON.stringify(issues)) + } +} + +const directory = process.argv[2] +checkDirectory(directory) diff --git a/src/parser.js b/src/parser.js new file mode 100644 index 0000000..7cbeb3d --- /dev/null +++ b/src/parser.js @@ -0,0 +1,5 @@ +import nhp from 'node-html-parser'; + +export const getTitle = (html) => { + return nhp.parse(html).querySelector('title').toString(); +} diff --git a/src/rules/Rule.js b/src/rules/Rule.js new file mode 100644 index 0000000..808b6f7 --- /dev/null +++ b/src/rules/Rule.js @@ -0,0 +1,17 @@ +export default class Rule { + constructor (filenames, options) { + this.options = options, + this.filenames = filenames + this.issueCount = 0 + this.issues = [] + this.initialize() + } + + report() { + return { + ruleName: this.constructor.name, + issueCount: this.issueCount, + issues: this.issues + } + } +} diff --git a/src/rules/TitleLength.js b/src/rules/TitleLength.js new file mode 100644 index 0000000..ae73181 --- /dev/null +++ b/src/rules/TitleLength.js @@ -0,0 +1,38 @@ +import Rule from './Rule.js' +import { extensionFilter, getContent } from '../helpers.js' +import { getTitle } from '../parser.js' + +export default class TitleLength extends Rule { + initialize() { + this.minLength = 30 + this.maxLength = 70 + this.filenames = extensionFilter(this.filenames, 'html'); + } + + report() { + return { + ...super.report(), + ...{ + issueDescription: `The <title> should contain between ${this.minLength} and ${this.maxLength} characters.`, + issueReferences: ['https://moz.com/learn/seo/title-tag', 'https://www.authorityhacker.com/seo-title-tags/', 'https://www.codeur.com/blog/seo-optimiser-title/'] + } + } + } + + check() { + for (const filename of this.filenames) { + const content = getContent(filename); + const title = getTitle(content) + if (this.test(title)) { + this.issueCount++ + const issue = { + url: filename.replace(this.options.directory, ''), + title: title + } + this.issues = [...this.issues, ...[issue]] + } + } + + return this.report() + } +} diff --git a/src/rules/TitleLengthTooLong.js b/src/rules/TitleLengthTooLong.js new file mode 100644 index 0000000..4f41e32 --- /dev/null +++ b/src/rules/TitleLengthTooLong.js @@ -0,0 +1,16 @@ +import TitleLength from './TitleLength.js' + +export default class TitleLengthTooLong extends TitleLength { + report() { + if (this.issueCount == 0) return + + return { + ...super.report(), + ...{ issueTitle: `Your title is to long on ${this.issueCount} page${this.issueCount == 1 ? '' : 's'}` } + } + } + + test(title) { + return title.length > this.maxLength + } +} diff --git a/src/rules/TitleLengthTooShort.js b/src/rules/TitleLengthTooShort.js new file mode 100644 index 0000000..d19d148 --- /dev/null +++ b/src/rules/TitleLengthTooShort.js @@ -0,0 +1,16 @@ +import TitleLength from './TitleLength.js' + +export default class TitleLengthTooShort extends TitleLength { + report() { + if (this.issueCount == 0) return + + return { + ...super.report(), + ...{ issueTitle: `Your title is to short on ${this.issueCount} page${this.issueCount == 1 ? '' : 's'}` } + } + } + + test(title) { + return title.length < this.minLength + } +} diff --git a/src/rules/TitleUnique.js b/src/rules/TitleUnique.js new file mode 100644 index 0000000..f141df3 --- /dev/null +++ b/src/rules/TitleUnique.js @@ -0,0 +1,42 @@ +import Rule from './Rule.js' +import { extensionFilter, getContent } from '../helpers.js' +import { getTitle } from '../parser.js' + +export default class TitleUnique extends Rule { + initialize() { + this.filenames = extensionFilter(this.filenames, 'html'); + } + + report() { + return { + ...super.report(), + ...{ + issueTitle: `The <title> should be unique for every page, ${this.issueCount} pages are concerned.`, + issueDescription: `You should change title on:`, + issueReferences: ['https://moz.com/learn/seo/title-tag'] + } + } + } + + check() { + let titles = {} + for (const filename of this.filenames) { + const content = getContent(filename); + const title = getTitle(content) + titles[title] = titles[title] ? [...titles[title], ...[filename]] : [filename] + } + + for (const title in titles) { + if (titles[title].length > 1) { + this.issueCount += titles[title].length + const issue = { + title: title, + url: titles[title] + } + this.issues = [...this.issues, ...[issue]] + } + } + + return this.report() + } +} diff --git a/src/rules/index.js b/src/rules/index.js new file mode 100644 index 0000000..eb80450 --- /dev/null +++ b/src/rules/index.js @@ -0,0 +1,11 @@ +import TitleUnique from './TitleUnique.js' +import TitleLengthTooLong from './TitleLengthTooLong.js' +import TitleLengthTooShort from './TitleLengthTooShort.js' + +export default function getRules(filesnames, options) { + return [ + new TitleUnique(filesnames, options), + new TitleLengthTooLong(filesnames, options), + new TitleLengthTooShort(filesnames, options), + ] +}