Skip to content

Commit

Permalink
feat: add require-open-graph-protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
yeonjuan committed Nov 19, 2023
1 parent 1120658 commit 078ecf6
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 9 deletions.
66 changes: 66 additions & 0 deletions docs/rules/require-open-graph-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
id: require-open-graph-protocol
title: "require-open-graph-protocol"
---

# require-open-graph-protocol

Enforce to use specified meta tags for open graph protocol.

## How to use

.eslintrc.js

```js
module.exports = {
rules: {
"@html-eslint/require-open-graph-protocol": "error",
},
};
```

## Rule Details

### Options

- `ogp` (default: ['og:title', 'og:type', 'og:url', 'og:image']): enforce to use specified open graph protocol meta tags

Examples of **incorrect** code for this rule with the default `{ "ogp": ['og:title', 'og:type', 'og:url', 'og:image'] }` options:

```html
<html>
<head>
<title>title</title>
</head>
</html>
```

<!-- prettier-ignore -->
```html
<html>
<head>
<title>title</title>
<!-- empty values -->
<meta property="og:url" />
<meta property="og:image" content="" />
</head>
</html>
```

Examples of **correct** code for this rule:

```html
<html>
<head>
<title>title</title>
<meta property="og:url" content="https://canonical-url.com" />
<meta property="og:type" content="video.movie" />
<meta property="og:title" content="title" />
<meta property="og:image" content="https://image.png" />
</head>
</html>
```

## Further reading

- [The Open Graph protocol](https://ogp.me/)
2 changes: 2 additions & 0 deletions packages/eslint-plugin/lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const requireAttrs = require("./require-attrs");
const noRestrictedAttrValues = require("./no-restricted-attr-values");
const noScriptStyleType = require("./no-script-style-type");
const lowercase = require("./lowercase");
const requireOpenGraphProtocol = require("./require-open-graph-protocol");

module.exports = {
"require-lang": requireLang,
Expand Down Expand Up @@ -70,4 +71,5 @@ module.exports = {
"no-restricted-attr-values": noRestrictedAttrValues,
"no-script-style-type": noScriptStyleType,
lowercase: lowercase,
"require-open-graph-protocol": requireOpenGraphProtocol,
};
135 changes: 135 additions & 0 deletions packages/eslint-plugin/lib/rules/require-open-graph-protocol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const { NODE_TYPES } = require("@html-eslint/parser");
const { RULE_CATEGORY } = require("../constants");
const { filter } = require("./utils/array");
const { findAttr } = require("./utils/node");

const MESSAGE_IDS = {
MISSING: "missing",
EMPTY: "empty",
};

const DEFAULT_REQUIRED_PROPERTIES = [
"og:title",
"og:type",
"og:url",
"og:image",
];

/**
* @param {string[]} properties
* @returns {string[]}
*/
function normalize(properties) {
return properties.map((prop) => {
if (prop.indexOf("og:") === 0) return prop;
return `og:${prop}`;
});
}

/**
* @type {Rule}
*/
module.exports = {
meta: {
type: "code",

docs: {
description: 'Enforce to use `<meta name="viewport">` in `<head>`',
category: RULE_CATEGORY.SEO,
recommended: false,
},

fixable: null,
schema: [
{
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
],
messages: {
[MESSAGE_IDS.MISSING]:
"Require use of meta tags for OGP. ({{properties}})",
[MESSAGE_IDS.EMPTY]: "Unexpected empty 'content' attribute",
},
},

create(context) {
/**
* @type {string[]}
*/
const requiredProperties = normalize(
(context.options && context.options[0]) || DEFAULT_REQUIRED_PROPERTIES
);

/**
* @param {ChildType<TagNode>} node
* @returns {node is TagNode}
*/
function isOgpMeta(node) {
const isMeta = node.type === NODE_TYPES.Tag && node.name === "meta";
const property = isMeta ? findAttr(node, "property") : undefined;
const hasOgProperty =
!!property &&
!!property.value &&
property.value.value.indexOf("og:") === 0;
return hasOgProperty;
}

return {
Tag(node) {
if (node.name !== "head") {
return;
}
const children = node.children;

const metaTags = filter(children, isOgpMeta);

const missingProperties = requiredProperties.filter((required) => {
return !metaTags.some((meta) => {
const property = findAttr(meta, "property");
if (property && property.value) {
return property.value.value === required;
}
return false;

Check warning on line 96 in packages/eslint-plugin/lib/rules/require-open-graph-protocol.js

View check run for this annotation

Codecov / codecov/patch

packages/eslint-plugin/lib/rules/require-open-graph-protocol.js#L96

Added line #L96 was not covered by tests
});
});

const emptyContentMetaTags = metaTags.filter((meta) => {
const property = findAttr(meta, "property");
if (
property &&
property.value &&
requiredProperties.includes(property.value.value)
) {
const content = findAttr(meta, "content");
return !content || !content.value || !content.value.value;
}
return false;
});

if (missingProperties.length) {
context.report({
node,
data: {
properties: missingProperties.join(", "),
},
messageId: MESSAGE_IDS.MISSING,
});
}

if (emptyContentMetaTags.length) {
emptyContentMetaTags.forEach((meta) => {
const content = findAttr(meta, "content");
context.report({
node: content || meta,
messageId: MESSAGE_IDS.EMPTY,
});
});
}
},
};
},
};
7 changes: 0 additions & 7 deletions packages/eslint-plugin/lib/rules/utils/node.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
/**
* @typedef {import("es-html-parser").TagNode} TagNode
* @typedef {import("es-html-parser").AnyNode} AnyNode
* @typedef {import("es-html-parser").TextNode} TextNode
* @typedef {import("es-html-parser").AttributeNode} AttributeNode
*/

module.exports = {
/**
* @param {TagNode | ScriptTagNode | StyleTagNode} node
Expand Down
4 changes: 2 additions & 2 deletions packages/eslint-plugin/lib/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import ESTree from "estree";
import ESLint from "eslint";
import * as ESHtml from "es-html-parser";
{
}

declare global {
type Fix = ESLint.Rule.Fix;
Expand Down Expand Up @@ -59,6 +57,8 @@ declare global {
}

interface AttributeNode extends ESHtml.AttributeNode {
key: AttributeKeyNode;
value?: AttributeValueNode;
parent: TagNode | StyleTagNode | ScriptTagNode;
}

Expand Down
125 changes: 125 additions & 0 deletions packages/eslint-plugin/tests/rules/require-open-graph-protocol.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const createRuleTester = require("../rule-tester");
const rule = require("../../lib/rules/require-open-graph-protocol");

const ruleTester = createRuleTester();

ruleTester.run("require-open-graph-protocol", rule, {
valid: [
{
code: "<html></html>",
},
{
code: `<html>
<head>
<meta property="og:url" content="title" />
<meta property="og:type" content="video.movie" />
<meta property="og:title" content="title" />
<meta property="og:image" content="https://image.png" />
</head>
</html>`,
},
],
invalid: [
{
code: "<html><head></head></html>",
errors: [
{
message:
"Require use of meta tags for OGP. (og:title, og:type, og:url, og:image)",
},
],
},
{
code: `<html>
<head>
<meta property="og:title" content="title">
</head>
</html>`,
errors: [
{
message:
"Require use of meta tags for OGP. (og:type, og:url, og:image)",
},
],
},
{
code: `<html>
<head>
<meta property="og:title" content="title" />
<meta property="og:image" content="https://image.png" />
</head>
</html>`,
errors: [
{
message: "Require use of meta tags for OGP. (og:type, og:url)",
},
],
},
{
code: `<html>
<head>
<meta property="og:title" content="title" />
<meta property="og:image" content="https://image.png" />
</head>
</html>`,
options: [["description"]],
errors: [
{
message: "Require use of meta tags for OGP. (og:description)",
},
],
},
{
code: `<html>
<head>
<meta property="og:title" content="title" />
<meta property="og:image" content="https://image.png" />
</head>
</html>`,
options: [["og:description", "og:video"]],
errors: [
{
message:
"Require use of meta tags for OGP. (og:description, og:video)",
},
],
},
{
code: `<html>
<head>
<meta property="og:url" content="title" />
<meta property="og:type" content="video.movie" />
<meta property="og:title" content="" />
<meta property="og:image" />
</head>
</html>`,
errors: [
{
messageId: "empty",
},
{
messageId: "empty",
},
],
},
{
code: `<html>
<head>
<meta property="og:url" />
<meta property="og:type" content="video.movie" />
<meta property="og:title" content="" />
<meta property="og:image" />
</head>
</html>`,
options: [["title", "og:image"]],
errors: [
{
messageId: "empty",
},
{
messageId: "empty",
},
],
},
],
});

0 comments on commit 078ecf6

Please sign in to comment.