Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Expose GIT Commit Data
uses: rlespinasse/git-commit-data-action@v1.x

- name: Install the Interpreter
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 14.x
node-version: 18.x
cache: yarn

- name: Load the Cached Dependencies
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
Expand All @@ -57,17 +57,17 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Install the Interpreter
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 14.x
node-version: 18.x
always-auth: true
cache: yarn

- name: Load the Cached Dependencies
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
Expand Down Expand Up @@ -96,10 +96,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
name: ${{ needs.publish.outputs.version }}
tag_name: ${{ needs.publish.outputs.version }}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "prerender-seo-scorer",
"version": "1.5.1",
"version": "1.5.2",
"description": "SEO Scoring from HTML, find and score SEO relevant tags, text, values.",
"keywords": [
"seo",
Expand Down
22 changes: 16 additions & 6 deletions src/checks/og-tags.check.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { CheerioAPI } from 'cheerio';
import { IChecker, ICheckerContext, IRecommendation } from '../interfaces';

const documentation = 'https://docs.prerender.io/docs/open-graph';

export const check_og_tags: IChecker = ({ $, raw_html }: ICheckerContext) => {
// Open Graph tags can be declared in several shapes that all appear in the wild:
// - canonical: <meta property="og:title" ...>
// - mixed-case value: <meta property="OG:Title" ...> (CMSes that title-case attribute values)
// - name fallback: <meta name="og:title" ...> (some CMSes / SEO plugins)
// The CSS Level 4 `i` flag matches the attribute value case-insensitively (cheerio supports it).
const findOgContent = ($: CheerioAPI, tag: string): string | undefined =>
$(`meta[property="og:${tag}" i]`).attr('content') ||
$(`meta[name="og:${tag}" i]`).attr('content');

export const check_og_tags: IChecker = ({ $ }: ICheckerContext) => {
const recommendations: IRecommendation[] = [];
let score_delta = 0;

// required fields
const ogTitle = $('meta[property="og:title"]').attr('content');
const ogType = $('meta[property="og:type"]').attr('content');
const ogImage = $('meta[property="og:image"]').attr('content');
const ogURL = $('meta[property="og:url"]').attr('content');
const ogTitle = findOgContent($, 'title');
const ogType = findOgContent($, 'type');
const ogImage = findOgContent($, 'image');
const ogURL = findOgContent($, 'url');

const penalty = -2;
const optionalPenalty = -1;
Expand Down Expand Up @@ -64,7 +74,7 @@ export const check_og_tags: IChecker = ({ $, raw_html }: ICheckerContext) => {
}

// optional fields
const ogDescription = $('meta[property="og:description"]').attr('content');
const ogDescription = findOgContent($, 'description');

if (!ogDescription) {
score_delta -= optionalPenalty;
Expand Down
83 changes: 78 additions & 5 deletions src/checks/og-tags.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { scorer } from '../scorer';
import { check_og_tags } from './og-tags.check';

describe('Og Tags Test', () => {
test('should not remove any points', () => {
describe('OG Tags check', () => {
test('awards full score when all required and optional og tags are present', () => {
const html = `
<html>
<head>
Expand All @@ -17,10 +17,10 @@ describe('Og Tags Test', () => {
const result = scorer(html, [check_og_tags]);

expect(result.score).toBe(10.0);
expect(result.recommendations.length).toBe(0);
expect(result.recommendations).toEqual([]);
});

test('should remove 2 point for each missing element', () => {
test('reports each absent required tag with its description and score delta', () => {
const html = `
<html>
<head>
Expand All @@ -31,6 +31,79 @@ describe('Og Tags Test', () => {
const result = scorer(html, [check_og_tags]);

expect(result.score).toBe(1);
expect(result.recommendations.length).toBe(3);
expect(result.recommendations.map((r) => r.description)).toEqual([
'Missing og:image meta tag',
'Missing og:url meta tag',
'Missing og:description meta tag',
]);
});

test.each<[tag: string, propertyValue: string]>([
['title', 'OG:Title'],
['title', 'og:Title'],
['type', 'OG:Type'],
['type', 'og:Type'],
['image', 'OG:Image'],
['image', 'og:Image'],
['url', 'OG:URL'],
['url', 'og:Url'],
['description', 'OG:Description'],
['description', 'og:Description'],
])(
'detects og:%s regardless of property-value case (property="%s")',
(tag, propertyValue) => {
const html = `<html><head><meta property="${propertyValue}" content="x" /></head></html>`;
const result = scorer(html, [check_og_tags]);

expect(result.recommendations.map((r) => r.description)).not.toContain(
`Missing og:${tag} meta tag`,
);
},
);

test.each<[tag: string, attribute: 'property' | 'name']>([
['title', 'property'],
['title', 'name'],
['type', 'property'],
['type', 'name'],
['image', 'property'],
['image', 'name'],
['url', 'property'],
['url', 'name'],
['description', 'property'],
['description', 'name'],
])(
'detects og:%s when declared with %s attribute',
(tag, attribute) => {
const html = `<html><head><meta ${attribute}="og:${tag}" content="x" /></head></html>`;
const result = scorer(html, [check_og_tags]);

expect(result.recommendations.map((r) => r.description)).not.toContain(
`Missing og:${tag} meta tag`,
);
},
);

test.each<[tag: string, attribute: 'property' | 'name']>([
['title', 'property'],
['title', 'name'],
['type', 'property'],
['type', 'name'],
['image', 'property'],
['image', 'name'],
['url', 'property'],
['url', 'name'],
['description', 'property'],
['description', 'name'],
])(
'reports og:%s as missing when its %s attribute has empty content',
(tag, attribute) => {
const html = `<html><head><meta ${attribute}="og:${tag}" content="" /></head></html>`;
const result = scorer(html, [check_og_tags]);

expect(result.recommendations.map((r) => r.description)).toContain(
`Missing og:${tag} meta tag`,
);
},
);
});
Loading