From cd9d539b778499477e8ed6f6041f856f4a81230a Mon Sep 17 00:00:00 2001 From: DanSnow Date: Wed, 30 Jun 2021 01:25:31 +0800 Subject: [PATCH] feat: add seo-tags --- docs/rules/seo-tags.md | 45 ++++++++++ lib/configs/recommended.js | 1 + lib/index.js | 1 + lib/rules/seo-tags.js | 71 ++++++++++++++++ tests/rules/seo-tags.spec.js | 156 +++++++++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 docs/rules/seo-tags.md create mode 100644 lib/rules/seo-tags.js create mode 100644 tests/rules/seo-tags.spec.js diff --git a/docs/rules/seo-tags.md b/docs/rules/seo-tags.md new file mode 100644 index 0000000..1f9d74c --- /dev/null +++ b/docs/rules/seo-tags.md @@ -0,0 +1,45 @@ +# warning about `component` not matching SEO spec + +Because this rule is guessing block type via finding `HeroBlock` tag, please notice that it may suggest a wrong fix. + +This rule has suggested fixes. + +## Fail + +For standard blocks: + +```vue + +``` + +For hero blocks: + +```vue + +``` + +## Pass + +For standard blocks: + +```vue + +``` + +For hero blocks: + +```vue + +``` diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js index a318527..7ce8442 100644 --- a/lib/configs/recommended.js +++ b/lib/configs/recommended.js @@ -8,5 +8,6 @@ module.exports = { '@storipress/block/headline-image-missing-alt': 'error', '@storipress/block/link-element-no-literal-href': 'warn', '@storipress/block/image-with-literal-src': 'warn', + '@storipress/block/seo-tags': 'warn', }, } diff --git a/lib/index.js b/lib/index.js index d6b0bc4..f6da355 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,7 @@ module.exports = { 'link-element-no-literal-href': require('./rules/link-element-no-literal-href'), 'image-with-literal-src': require('./rules/image-with-literal-src'), 'image-without-src': require('./rules/image-without-src'), + 'seo-tags': require('./rules/seo-tags'), }, configs: { recommended: require('./configs/recommended'), diff --git a/lib/rules/seo-tags.js b/lib/rules/seo-tags.js new file mode 100644 index 0000000..c24e9a1 --- /dev/null +++ b/lib/rules/seo-tags.js @@ -0,0 +1,71 @@ +const { findLiteralAttr } = require('../helpers/find-attr') + +const TAGS = { + hero: ['h4', 'h2'], + block: ['h4', 'h3'], +} + +module.exports = { + meta: { + type: 'problem', + + docs: { + description: 'checking seo tag to follow SEO spec', + category: 'Possible Errors', + recommended: true, + }, + messages: { + tagNotMatch: 'here possible should be `{{ tag }}`, please check again', + changeTag: 'change this to {{ tag }}', + }, + schema: [], // no options + }, + create(context) { + const filename = context.getFilename() + + // guessing block type + let blockType = filename.startsWith('navbar') ? 'hero' : 'block' + const sourceCode = context.getSourceCode() + + function checkSEOTag(node) { + const isTitle = sourceCode.getText(node).includes('article.title') + const attr = findLiteralAttr(node, 'component') + + if (!attr) { + return + } + + const expectedTag = TAGS[blockType][isTitle ? 1 : 0] + + const data = { tag: expectedTag } + + if (attr.value.value !== expectedTag) { + context.report({ + node: attr, + messageId: 'tagNotMatch', + data, + suggest: [ + { + messageId: 'changeTag', + data, + fix(fixer) { + return fixer.replaceText(attr, `component="${expectedTag}"`) + }, + }, + ], + }) + } + } + + return context.parserServices.defineTemplateBodyVisitor({ + 'VElement[rawName=Block]'() { + blockType = 'block' + }, + 'VElement[rawName=HeroBlock]'() { + blockType = 'hero' + }, + 'VElement[rawName=TextInput]': checkSEOTag, + 'VElement[rawName=TextElement]': checkSEOTag, + }) + }, +} diff --git a/tests/rules/seo-tags.spec.js b/tests/rules/seo-tags.spec.js new file mode 100644 index 0000000..242e139 --- /dev/null +++ b/tests/rules/seo-tags.spec.js @@ -0,0 +1,156 @@ +const { ruleTester } = require('../_helper') +const rule = require('../../lib/rules/seo-tags') + +ruleTester.run('seo-tags', rule, { + valid: [ + { + code: ` + + `, + filename: 'test.vue', + }, + { + code: ` + + `, + filename: 'test.vue', + }, + { + code: ` + + `, + filename: 'test.vue', + }, + { + code: ` + + `, + filename: 'test.vue', + }, + { + code: ` + + `, + filename: 'test.vue', + }, + { + code: ` + + `, + filename: 'test.vue', + }, + ], + invalid: [ + { + code: ` + + `, + filename: 'test.vue', + errors: [ + { + messageId: 'tagNotMatch', + data: { tag: 'h3' }, + suggestions: [ + { + messageId: 'changeTag', + data: { tag: 'h3' }, + output: ` + + `, + }, + ], + }, + ], + }, + { + code: ` + + `, + filename: 'test.vue', + errors: [ + { + messageId: 'tagNotMatch', + data: { tag: 'h4' }, + suggestions: [ + { + messageId: 'changeTag', + data: { tag: 'h4' }, + output: ` + + `, + }, + ], + }, + ], + }, + { + code: ` + + `, + filename: 'test.vue', + errors: [ + { + messageId: 'tagNotMatch', + data: { tag: 'h4' }, + suggestions: [ + { + messageId: 'changeTag', + data: { tag: 'h4' }, + output: ` + + `, + }, + ], + }, + ], + }, + ], +})