diff --git a/LICENSE.md b/LICENSE.md index 1bb5f69..2c5f514 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Thiago Santos +Copyright (c) 2017 Gustavo Quinalha Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin/seotopper.js b/bin/seotopper.js new file mode 100644 index 0000000..13ca945 --- /dev/null +++ b/bin/seotopper.js @@ -0,0 +1,198 @@ +#!/usr/bin/env node + +const inquirer = require('inquirer') +const seotopper = require('../lib/seotopper') + +const questions = [ + { + type: 'input', + name: 'title', + message: 'What\'s the title of your page', + validate: value => { + if (value === '') { + return 'Please enter a title' + } + if (value.length > 57) { + return 'Please enter a title with less than 57 characters' + } + return true + } + }, + { + type: 'input', + name: 'description', + message: 'What\'s the description of your page', + validate: value => { + if (value === '') { + return 'Please enter a description' + } + if (value.length > 160) { + return 'Please enter a description with less than 160 characters' + } + return true + } + }, + { + type: 'input', + name: 'author', + message: 'Who\'s the author', + validate: value => { + if (value === '') { + return 'Please enter an author\'s name' + } + return true + } + }, + { + type: 'input', + name: 'image', + message: 'What\'s the image of your page', + validate: value => { + if (value === '') { + return 'Please enter an image' + } + return true + } + }, + { + type: 'input', + name: 'canonical', + message: 'What\'s the canonical url of your page', + validate: value => { + if (value === '') { + return 'Please enter a canonical url' + } + return true + } + }, + { + type: 'list', + name: 'robots', + message: 'What you wanna tell to the robots', + choices: [ + 'index/follow', + 'noindex/follow', + 'index/nofollow', + 'noarchive', + 'nosnippet', + 'noodp', + 'notranslate', + 'noimageindex', + 'none' + ] + }, + { + type: 'input', + name: 'base', + message: 'What\'s the base url of your page' + }, + { + type: 'input', + name: 'sitemap', + message: 'What\'s the sitemap of your page' + }, + { + type: 'input', + name: 'themeColor', + message: 'What\'s the theme-color of your page' + }, + { + type: 'confirm', + name: 'facebook', + message: 'Do you wanna seo for facebook' + }, + { + type: 'list', + name: 'facebookType', + message: 'What\'s the type of your page', + choices: [ + 'website', + 'blog', + 'article', + 'activity', + 'sport', + 'company', + 'restaurant', + 'hotel', + 'cause', + 'band', + 'government', + 'non_profit', + 'school', + 'university', + 'actor', + 'athlete', + 'city', + 'country', + 'album', + 'book', + 'drink', + 'game', + 'product', + 'song', + 'movie' + ], + when: answers => { + return answers.facebook + } + }, + { + type: 'input', + name: 'facebookSiteName', + message: 'What\'s the name of your page on facebook', + when: answers => { + return answers.facebook + } + }, + { + type: 'input', + name: 'facebookLocale', + message: 'What\'s the locale of your page on facebook', + when: answers => { + return answers.facebook + } + }, + { + type: 'input', + name: 'facebookId', + message: 'What\'s the id of your page on facebook', + when: answers => { + return answers.facebook + } + }, + { + type: 'input', + name: 'facebookAdmins', + message: 'What are the admins of your page on facebook', + when: answers => { + return answers.facebook + } + }, + { + type: 'confirm', + name: 'twitter', + message: 'Do you wanna seo for twitter' + }, + { + type: 'list', + name: 'twitterCard', + message: 'Which twitter do you want', + choices: [ + 'Summary', + 'Product', + 'Photo', + 'Summary Large Image', + 'Player', + 'App', + 'Gallery' + ], + when: answers => { + return answers.twitter + } + } +] + +inquirer.prompt(questions).then(answers => { + const generatedSEO = seotopper(answers) + process.stdout.write(generatedSEO) +}) diff --git a/lib/seotopper.js b/lib/seotopper.js index 525f778..01743b5 100644 --- a/lib/seotopper.js +++ b/lib/seotopper.js @@ -1,15 +1,23 @@ const utils = require('./utils') -const checkMissingKeys = utils.checkMissingKeys -const createErrorMessage = utils.createErrorMessage const requiredProperties = utils.requiredProperties +const checkMissingKeys = utils.checkMissingKeys +const createMissingKeysErrorMessage = utils.createMissingKeysErrorMessage +const checkEmptyKeys = utils.checkEmptyKeys +const createEmptyKeysErrorMessage = utils.createEmptyKeysErrorMessage function seotopper(args) { const keys = Object.keys(args) const missingKeys = checkMissingKeys(requiredProperties, keys) if (missingKeys.length > 0) { - throw new Error(createErrorMessage(missingKeys)) + throw new Error(createMissingKeysErrorMessage(missingKeys)) + } + + const emptyKeys = checkEmptyKeys(requiredProperties, keys, args) + + if (emptyKeys.length > 0) { + throw new Error(createEmptyKeysErrorMessage(emptyKeys)) } return `${args.title} @@ -35,21 +43,33 @@ function seotopper(args) { ${args.facebook ? ` - + - - + + ${args.facebookSiteName ? + `` : + '' + } - - - ` : + ${args.facebookLocale ? + `` : + '' + } + ${args.facebookId ? + `` : + '' + } + ${args.facebookAdmins ? + `` : + '' + }` : '' } ${args.twitter ? ` - + diff --git a/lib/utils.js b/lib/utils.js index 68836d2..689628b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -10,15 +10,27 @@ const requiredProperties = [ const checkMissingKeys = (requiredProperties, keys) => requiredProperties .filter(property => keys.indexOf(property) === -1) -const createErrorMessage = missingKeys => String( +const createMissingKeysErrorMessage = missingKeys => String( 'The following ' + (missingKeys.length > 1 ? 'properties are' : 'property is') + ' required: ' + - missingKeys.join(', ') + missingKeys.sort().join(', ') ) +const checkEmptyKeys = (requiredProperties, keys, args) => keys + .filter(key => requiredProperties.indexOf(key) !== -1) + .filter(key => args[key] === '') + +const createEmptyKeysErrorMessage = emptyKeys => String( + 'The following ' + + (emptyKeys.length > 1 ? 'properties' : 'property') + + ' cannot be empty: ' + + emptyKeys.sort().join(', ') +) module.exports = { checkMissingKeys, + checkEmptyKeys, requiredProperties, - createErrorMessage + createMissingKeysErrorMessage, + createEmptyKeysErrorMessage } diff --git a/package.json b/package.json index 7acefb7..f206cd3 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "author": "Gustavo Quinalha", "keywords": [], "license": "MIT", + "bin": { + "seotopper": "bin/seotopper.js" + }, "main": "lib/seotopper.js", "scripts": { "precommit": "npm test", @@ -31,7 +34,10 @@ "husky": "^0.13.3", "xo": "^0.18.1" }, - "engines" : { + "engines": { "node": ">=4" + }, + "dependencies": { + "inquirer": "^3.0.6" } } diff --git a/test/seotopper.test.js b/test/seotopper.test.js index 46657fb..285d0b7 100644 --- a/test/seotopper.test.js +++ b/test/seotopper.test.js @@ -12,16 +12,14 @@ test('main funcionality', t => { robots: 'index/follow', themeColor: '#f00', image: 'https://sua-url.com.br/images/intro.jpg', - facebook: { - type: 'website', - siteName: 'Exemplo', - locale: 'pt_BR', - id: '5349', - admins: '123456789' - }, - twitter: { - card: 'summary' - } + facebook: true, + facebookType: 'website', + facebookSiteName: 'Exemplo', + facebookLocale: 'pt_BR', + facebookId: '5349', + facebookAdmins: '123456789', + twitter: true, + twitterCard: 'summary' }) const expected = `Título da minha página @@ -42,7 +40,7 @@ test('main funcionality', t => { - + @@ -79,16 +77,14 @@ test('base should be optional', t => { robots: 'index/follow', themeColor: '#f00', image: 'https://sua-url.com.br/images/intro.jpg', - facebook: { - type: 'website', - siteName: 'Exemplo', - locale: 'pt_BR', - id: '5349', - admins: '123456789' - }, - twitter: { - card: 'summary' - } + facebook: true, + facebookType: 'website', + facebookSiteName: 'Exemplo', + facebookLocale: 'pt_BR', + facebookId: '5349', + facebookAdmins: '123456789', + twitter: true, + twitterCard: 'summary' }) t.notRegex(actual, /rel="base"/) @@ -103,16 +99,14 @@ test('sitemap should be optional', t => { robots: 'index/follow', themeColor: '#f00', image: 'https://sua-url.com.br/images/intro.jpg', - facebook: { - type: 'website', - siteName: 'Exemplo', - locale: 'pt_BR', - id: '5349', - admins: '123456789' - }, - twitter: { - card: 'summary' - } + facebook: true, + facebookType: 'website', + facebookSiteName: 'Exemplo', + facebookLocale: 'pt_BR', + facebookId: '5349', + facebookAdmins: '123456789', + twitter: true, + twitterCard: 'summary' }) t.notRegex(actual, /rel="sitemap"/) @@ -126,16 +120,14 @@ test('themeColor should be optional', t => { canonical: 'https://sua-url.com.br', robots: 'index/follow', image: 'https://sua-url.com.br/images/intro.jpg', - facebook: { - type: 'website', - siteName: 'Exemplo', - locale: 'pt_BR', - id: '5349', - admins: '123456789' - }, - twitter: { - card: 'summary' - } + facebook: true, + facebookType: 'website', + facebookSiteName: 'Exemplo', + facebookLocale: 'pt_BR', + facebookId: '5349', + facebookAdmins: '123456789', + twitter: true, + twitterCard: 'summary' }) t.notRegex(actual, /name="theme-color"/) @@ -151,9 +143,8 @@ test('facebook should be optional', t => { canonical: 'https://sua-url.com.br', robots: 'index/follow', image: 'https://sua-url.com.br/images/intro.jpg', - twitter: { - card: 'summary' - } + twitter: true, + twitterCard: 'summary' }) t.notRegex(actual, /property="og:type"/) t.notRegex(actual, /property="og:title"/) @@ -174,13 +165,12 @@ test('twitter should be optional', t => { canonical: 'https://sua-url.com.br', robots: 'index/follow', image: 'https://sua-url.com.br/images/intro.jpg', - facebook: { - type: 'website', - siteName: 'Exemplo', - locale: 'pt_BR', - id: '5349', - admins: '123456789' - } + facebook: true, + facebookType: 'website', + facebookSiteName: 'Exemplo', + facebookLocale: 'pt_BR', + facebookId: '5349', + facebookAdmins: '123456789' }) t.notRegex(actual, /name="twitter:card"/) t.notRegex(actual, /name="twitter:title"/) @@ -270,3 +260,94 @@ test('should give feedback for more than one required property', t => { }) }, 'The following properties are required: author, description, title') }) + +test('title cannot be empty', t => { + t.throws(() => { + seotopper({ + title: '', + description: 'Descrição da minha página', + author: 'Eu', + canonical: 'https://sua-url.com.br', + robots: 'index/follow', + image: 'https://sua-url.com.br/images/intro.jpg' + }) + }, 'The following property cannot be empty: title') +}) + +test('description cannot be empty', t => { + t.throws(() => { + seotopper({ + title: 'Título da minha página', + description: '', + author: 'Eu', + canonical: 'https://sua-url.com.br', + robots: 'index/follow', + image: 'https://sua-url.com.br/images/intro.jpg' + }) + }, 'The following property cannot be empty: description') +}) + +test('author cannot be empty', t => { + t.throws(() => { + seotopper({ + title: 'Título da minha página', + description: 'Descrição da minha página', + author: '', + canonical: 'https://sua-url.com.br', + robots: 'index/follow', + image: 'https://sua-url.com.br/images/intro.jpg' + }) + }, 'The following property cannot be empty: author') +}) + +test('canonical cannot be empty', t => { + t.throws(() => { + seotopper({ + title: 'Título da minha página', + author: 'Eu', + description: 'Descrição da minha página', + canonical: '', + robots: 'index/follow', + image: 'https://sua-url.com.br/images/intro.jpg' + }) + }, 'The following property cannot be empty: canonical') +}) + +test('robots cannot be empty', t => { + t.throws(() => { + seotopper({ + title: 'Título da minha página', + author: 'Eu', + description: 'Descrição da minha página', + canonical: 'https://sua-url.com.br', + robots: '', + image: 'https://sua-url.com.br/images/intro.jpg' + }) + }, 'The following property cannot be empty: robots') +}) + +test('image cannot be empty', t => { + t.throws(() => { + seotopper({ + title: 'Título da minha página', + author: 'Eu', + description: 'Descrição da minha página', + canonical: 'https://sua-url.com.br', + robots: 'index/follow', + image: '' + }) + }, 'The following property cannot be empty: image') +}) + +test('should give feedback for more than one empty property', t => { + t.throws(() => { + seotopper({ + title: '', + description: '', + author: '', + canonical: 'https://sua-url.com.br', + robots: 'index/follow', + image: 'https://sua-url.com.br/images/intro.jpg' + }) + }, 'The following properties cannot be empty: author, description, title') +}) diff --git a/test/utils.test.js b/test/utils.test.js index 9107da2..9adc97f 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -2,7 +2,9 @@ const test = require('ava') const { requiredProperties, checkMissingKeys, - createErrorMessage + createMissingKeysErrorMessage, + checkEmptyKeys, + createEmptyKeysErrorMessage } = require('../lib/utils') test('requiredProperties should be an array', t => { @@ -18,16 +20,44 @@ test('checkMissingKeys', t => { t.deepEqual(actual, expected, 'should return the missing keys') }) -test('createErrorMessage missing one key', t => { - const actual = createErrorMessage(['title']) +test('createMissingKeysErrorMessage missing one key', t => { + const actual = createMissingKeysErrorMessage(['title']) const expected = 'The following property is required: title' t.is(actual, expected) }) -test('createErrorMessage missign more than one key', t => { - const actual = createErrorMessage(['title', 'author']) - const expected = 'The following properties are required: title, author' +test('createMissingKeysErrorMessage missing more than one key', t => { + const actual = createMissingKeysErrorMessage(['title', 'author']) + const expected = 'The following properties are required: author, title' + + t.is(actual, expected) +}) + +test('checkEmptyKeys', t => { + const actual = checkEmptyKeys( + ['title', 'author'], + ['title', 'author'], + { + title: '', + author: 'me' + } + ) + const expected = ['title'] + + t.deepEqual(actual, expected) +}) + +test('createEmptyKeysErrorMessage with one empty key', t => { + const actual = createEmptyKeysErrorMessage(['title']) + const expected = 'The following property cannot be empty: title' + + t.is(actual, expected) +}) + +test('createEmptyKeysErrorMessage with more than one empty key', t => { + const actual = createEmptyKeysErrorMessage(['title', 'author']) + const expected = 'The following properties cannot be empty: author, title' t.is(actual, expected) })