├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── lerna.json ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── bin │ │ ├── cli │ │ │ ├── build.js │ │ │ ├── get-url.js │ │ │ ├── help.js │ │ │ └── index.js │ │ ├── color.js │ │ └── view │ │ │ ├── index.js │ │ │ └── render.js │ └── package.json └── core │ ├── CHANGELOG.md │ ├── package.json │ └── src │ ├── get-browserless.js │ ├── get-urls.js │ ├── index.js │ ├── rules.js │ └── validators.js ├── pnpm-workspace.yaml └── static ├── banner.png ├── banner.sketch └── demo.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 100 13 | indent_brace_style = 1TBS 14 | spaces_around_operators = true 15 | quote_type = auto 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | # Check for updates to GitHub Actions every weekday 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | - name: Setup PNPM 22 | uses: pnpm/action-setup@v4 23 | with: 24 | version: latest 25 | run_install: true 26 | - name: Test 27 | run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # npm 3 | ############################ 4 | node_modules 5 | npm-debug.log 6 | .node_history 7 | yarn.lock 8 | package-lock.json 9 | 10 | ############################ 11 | # tmp, editor & OS files 12 | ############################ 13 | .tmp 14 | *.swo 15 | *.swp 16 | *.swn 17 | *.swm 18 | .DS_Store 19 | *# 20 | *~ 21 | .idea 22 | *sublime* 23 | nbproject 24 | 25 | ############################ 26 | # Tests 27 | ############################ 28 | testApp 29 | coverage 30 | .nyc_output 31 | 32 | ############################ 33 | # Other 34 | ############################ 35 | .envrc 36 | typescript -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix=~ 2 | save=false 3 | strict-peer-dependencies=false 4 | unsafe-perm=true 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.0.1](https://github.com/microlinkhq/metatags/compare/v1.0.0...v1.0.1) (2021-11-12) 7 | 8 | **Note:** Version bump only for package metatags 9 | 10 | 11 | 12 | 13 | 14 | # [1.0.0](https://github.com/microlinkhq/metatags/compare/v0.1.9...v1.0.0) (2021-09-14) 15 | 16 | **Note:** Version bump only for package metatags 17 | 18 | 19 | 20 | 21 | 22 | ## [0.1.9](https://github.com/microlinkhq/metatags/compare/v0.1.8...v0.1.9) (2021-09-03) 23 | 24 | **Note:** Version bump only for package metatags 25 | 26 | 27 | 28 | 29 | 30 | ## [0.1.8](https://github.com/microlinkhq/metatags/compare/v0.1.7...v0.1.8) (2021-09-02) 31 | 32 | **Note:** Version bump only for package metatags 33 | 34 | 35 | 36 | 37 | 38 | ## [0.1.7](https://github.com/microlinkhq/metatags/compare/v0.1.6...v0.1.7) (2021-09-02) 39 | 40 | **Note:** Version bump only for package metatags 41 | 42 | 43 | 44 | 45 | 46 | ## [0.1.6](https://github.com/microlinkhq/metatags/compare/v0.1.5...v0.1.6) (2021-09-02) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * filepath ([ae8ae83](https://github.com/microlinkhq/metatags/commit/ae8ae83ade4a6705b9657f127fe5d2244ae9ef28)) 52 | 53 | 54 | 55 | 56 | 57 | ## [0.1.5](https://github.com/microlinkhq/metatags/compare/v0.1.4...v0.1.5) (2021-09-02) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * downgrade dependency ([0848020](https://github.com/microlinkhq/metatags/commit/08480205f49508930c02a2cca83810c3f292d2d4)) 63 | 64 | 65 | 66 | 67 | 68 | ## [0.1.4](https://github.com/microlinkhq/metatags/compare/v0.1.3...v0.1.4) (2021-09-02) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * add missing dependencies ([63b4468](https://github.com/microlinkhq/metatags/commit/63b4468640f380a61f80f4370d7a5252e7b95714)) 74 | 75 | 76 | 77 | 78 | 79 | ## [0.1.3](https://github.com/microlinkhq/metatags/compare/v0.1.2...v0.1.3) (2021-09-02) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * lerna for public packages ([9f5c67d](https://github.com/microlinkhq/metatags/commit/9f5c67d70fc72ac7767fa6b59a3209f76645a157)) 85 | 86 | 87 | 88 | 89 | 90 | ## [0.1.2](https://github.com/microlinkhq/metatags/compare/v0.1.1...v0.1.2) (2021-09-02) 91 | 92 | **Note:** Version bump only for package metatags 93 | 94 | 95 | 96 | 97 | 98 | ## [0.1.1](https://github.com/microlinkhq/metatags/compare/v0.1.0...v0.1.1) (2021-07-23) 99 | 100 | **Note:** Version bump only for package metatags 101 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2021 Microlink (microlink.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | metatags 4 |
5 |
6 |

7 | 8 | ![Last version](https://img.shields.io/github/tag/microlinkhq/metatags.svg?style=flat-square) 9 | [![NPM Status](https://img.shields.io/npm/dm/metatags.svg?style=flat-square)](https://www.npmjs.org/package/metatags) 10 | 11 | > Ensure your HTML is previewed beautifully across social networks. 12 | 13 | ![](/static/demo.png) 14 | 15 | With **metatags** you can be sure your content will be well defined and it will look beautiful on Google, Facebook, Twitter, and more. 16 | 17 | ## Usage 18 | 19 | **Passing a URL** 20 | 21 | ``` 22 | npx metatags https://microlink.io 23 | ``` 24 | 25 | **Passing multiple URLs** 26 | 27 | ``` 28 | npx metatags https://microlink.io https://microlink.io/blog 29 | ``` 30 | 31 | **Providing a sitemap** 32 | 33 | ``` 34 | npx metatags https://microlink.io/sitemap.xml 35 | ``` 36 | 37 | ## License 38 | 39 | **metatags** © [Microlink](https://microlink.io), released under the [MIT](https://github.com/microlinkhq/metatags/blob/master/LICENSE.md) License.
40 | Authored and maintained by [Microlink](https://microlink.io) with help from [contributors](https://github.com/microlinkhq/metatags/contributors). 41 | 42 | > [microlink.io](https://microlink.io) · GitHub [microlinkhq](https://github.com/microlinkhq) · Twitter [@microlinkhq](https://twitter.com/microlinkhq) 43 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.0.1", 6 | "command": { 7 | "bootstrap": { 8 | "npmClientArgs": [ 9 | "--no-package-lock" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metatags", 3 | "description": "Ensure your HTML is previewed beautifully across social networks", 4 | "homepage": "https://metatags.co", 5 | "version": "0.0.0", 6 | "author": { 7 | "email": "hello@microlink.io", 8 | "name": "microlink.io", 9 | "url": "https://microlink.io" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Kiko Beats", 14 | "email": "josefrancisco.verdu@gmail.com" 15 | } 16 | ], 17 | "devDependencies": { 18 | "@commitlint/cli": "latest", 19 | "@commitlint/config-conventional": "latest", 20 | "@lerna-lite/cli": "latest", 21 | "@lerna-lite/run": "latest", 22 | "finepack": "latest", 23 | "git-authors-cli": "latest", 24 | "nano-staged": "latest", 25 | "npm-check-updates": "latest", 26 | "nyc": "latest", 27 | "prettier-standard": "latest", 28 | "simple-git-hooks": "latest", 29 | "standard": "latest", 30 | "standard-markdown": "latest" 31 | }, 32 | "engines": { 33 | "node": ">= 12" 34 | }, 35 | "scripts": { 36 | "build": "gulp build", 37 | "clean": "pnpm --recursive --parallel exec -- rm -rf node_modules", 38 | "contributors": "npm run contributors:add && npm run contributors:commit", 39 | "contributors:add": "pnpm --recursive --parallel exec -- finepack --sort-ignore-object-at ava", 40 | "contributors:commit": "(npx git-authors-cli && npx finepack --sort-ignore-object-at ava && git add package.json && git commit -m 'build: contributors' --no-verify) || true", 41 | "dev": "concurrently \"gulp\" \"npm run dev:server\"", 42 | "dev:server": "browser-sync start --server --files \"index.html, README.md, static/**/*.(css|js)\"", 43 | "lint": "standard-markdown README.md && standard", 44 | "prerelease": "npm run contributors", 45 | "pretest": "npm run lint", 46 | "release": "lerna publish --yes --sort --conventional-commits -m \"chore(release): %s\" --create-release github", 47 | "test": "lerna run test", 48 | "update": "pnpm --recursive --parallel exec ncu -u", 49 | "update:check": "pnpm --recursive --parallel exec ncu -errorLevel 2" 50 | }, 51 | "private": true, 52 | "license": "MIT", 53 | "commitlint": { 54 | "extends": [ 55 | "@commitlint/config-conventional" 56 | ] 57 | }, 58 | "nano-staged": { 59 | "*.js": [ 60 | "prettier-standard" 61 | ], 62 | "package.json": [ 63 | "finepack" 64 | ] 65 | }, 66 | "simple-git-hooks": { 67 | "commit-msg": "npx commitlint --edit", 68 | "pre-commit": "npx nano-staged" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.0.1](https://github.com/microlinkhq/metatags/compare/v1.0.0...v1.0.1) (2021-11-12) 7 | 8 | **Note:** Version bump only for package metatags 9 | 10 | 11 | 12 | 13 | 14 | # [1.0.0](https://github.com/microlinkhq/metatags/compare/v0.1.9...v1.0.0) (2021-09-14) 15 | 16 | **Note:** Version bump only for package metatags 17 | 18 | 19 | 20 | 21 | 22 | ## [0.1.9](https://github.com/microlinkhq/metatags/compare/v0.1.8...v0.1.9) (2021-09-03) 23 | 24 | **Note:** Version bump only for package @metatags/cli 25 | 26 | 27 | 28 | 29 | 30 | ## [0.1.8](https://github.com/microlinkhq/metatags/compare/v0.1.7...v0.1.8) (2021-09-02) 31 | 32 | **Note:** Version bump only for package @metatags/cli 33 | 34 | 35 | 36 | 37 | 38 | ## [0.1.7](https://github.com/microlinkhq/metatags/compare/v0.1.6...v0.1.7) (2021-09-02) 39 | 40 | **Note:** Version bump only for package @metatags/cli 41 | 42 | 43 | 44 | 45 | 46 | ## [0.1.6](https://github.com/microlinkhq/metatags/compare/v0.1.5...v0.1.6) (2021-09-02) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * filepath ([ae8ae83](https://github.com/microlinkhq/metatags/commit/ae8ae83ade4a6705b9657f127fe5d2244ae9ef28)) 52 | 53 | 54 | 55 | 56 | 57 | ## [0.1.5](https://github.com/microlinkhq/metatags/compare/v0.1.4...v0.1.5) (2021-09-02) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * downgrade dependency ([0848020](https://github.com/microlinkhq/metatags/commit/08480205f49508930c02a2cca83810c3f292d2d4)) 63 | 64 | 65 | 66 | 67 | 68 | ## [0.1.4](https://github.com/microlinkhq/metatags/compare/v0.1.3...v0.1.4) (2021-09-02) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * add missing dependencies ([63b4468](https://github.com/microlinkhq/metatags/commit/63b4468640f380a61f80f4370d7a5252e7b95714)) 74 | 75 | 76 | 77 | 78 | 79 | ## [0.1.3](https://github.com/microlinkhq/metatags/compare/v0.1.2...v0.1.3) (2021-09-02) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * lerna for public packages ([9f5c67d](https://github.com/microlinkhq/metatags/commit/9f5c67d70fc72ac7767fa6b59a3209f76645a157)) 85 | 86 | 87 | 88 | 89 | 90 | ## [0.1.2](https://github.com/microlinkhq/metatags/compare/v0.1.1...v0.1.2) (2021-09-02) 91 | 92 | **Note:** Version bump only for package @metatags/cli 93 | 94 | 95 | 96 | 97 | 98 | ## [0.1.1](https://github.com/microlinkhq/metatags/compare/v0.1.0...v0.1.1) (2021-07-23) 99 | 100 | **Note:** Version bump only for package @metatags/cli 101 | -------------------------------------------------------------------------------- /packages/cli/bin/cli/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Build = require('github-build') 4 | 5 | const { jobUrl, repo, sha } = require('ci-env') 6 | 7 | const MESSAGE = { 8 | START: 'Checking URLs availability…', 9 | PASS: 'Yours links are fine', 10 | FAIL: 'Something is wrong in your links', 11 | ERROR: 'Uh, something unexpected happened' 12 | } 13 | 14 | const EXIT_CODE_METHOD_MAPPER = { 15 | 0: 'pass', 16 | 1: 'fail' 17 | } 18 | 19 | const token = 20 | process.env.gh_token || 21 | process.env.GH_TOKEN || 22 | process.env.github_token || 23 | process.env.GITHUB_TOKEN || 24 | process.env.metatags_github_token || 25 | process.env.metatags_GITHUB_TOKEN 26 | 27 | const meta = { 28 | repo, 29 | sha, 30 | token, 31 | label: 'metatags', 32 | description: MESSAGE.START, 33 | url: jobUrl 34 | } 35 | 36 | const noopBuild = { 37 | start: () => Promise.resolve(), 38 | pass: () => Promise.resolve(), 39 | fail: () => Promise.resolve(), 40 | error: () => Promise.resolve() 41 | } 42 | 43 | const handleError = err => { 44 | if (err.status !== 404) { 45 | const message = `\n\nCould not add github status (${err.status}): ${err.error.message}` 46 | console.error(message) 47 | } 48 | } 49 | 50 | const createBuild = build => ({ 51 | pass: () => build.pass(MESSAGE.PASS).catch(handleError), 52 | fail: () => build.fail(MESSAGE.FAIL).catch(handleError), 53 | error: () => build.error(MESSAGE.ERROR).catch(handleError), 54 | start: () => build.start(MESSAGE.START).catch(handleError) 55 | }) 56 | 57 | const createExit = build => async ({ buildCode = 0, exitCode = buildCode }) => { 58 | const method = EXIT_CODE_METHOD_MAPPER[buildCode] || 'error' 59 | await build[method]() 60 | process.exit(exitCode) 61 | } 62 | 63 | const build = createBuild(token ? new Build(meta) : noopBuild) 64 | build.exit = createExit(build) 65 | 66 | module.exports = build 67 | -------------------------------------------------------------------------------- /packages/cli/bin/cli/get-url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = input => { 4 | const value = input || process.env.DEPLOY_URL || process.env.DEPLOY_PRIME_URL || process.env.URL 5 | return Array.isArray(value) ? value : value.split(',').map(item => item.trim()) 6 | } 7 | -------------------------------------------------------------------------------- /packages/cli/bin/cli/help.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { description } = require('../../package.json') 4 | const { pink, gray } = require('../color') 5 | 6 | module.exports = gray(`${gray(description)}. 7 | 8 | Usage 9 | $ ${pink('metatags')} [] 10 | 11 | Flags 12 | -c, --concurrence Number of concurrent petitions (defaults to 8). 13 | -f, --followRedirect Redirect responses should be followed (defaults to true). 14 | -h, --help Show the help information. 15 | -r, --retries Number of request retries when network errors happens (defaults to 2). 16 | -t, --timeout Milliseconds to wait before consider a timeout response. 17 | -p, --prerender Enable or disable prerendering for getting HTML markup (defaults to auto).`) 18 | -------------------------------------------------------------------------------- /packages/cli/bin/cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const { omit, isEmpty } = require('lodash') 6 | const beautyError = require('beauty-error') 7 | const JoyCon = require('joycon') 8 | 9 | const pkg = require('../../package.json') 10 | 11 | const joycon = new JoyCon({ 12 | packageKey: pkg.name, 13 | files: [ 14 | 'package.json', 15 | `.${pkg.name}rc`, 16 | `.${pkg.name}rc.json`, 17 | `.${pkg.name}rc.js`, 18 | `${pkg.name}.config.js` 19 | ] 20 | }) 21 | 22 | const metatags = require('@metatags/core') 23 | 24 | const getUrl = require('./get-url') 25 | const build = require('./build') 26 | const view = require('../view') 27 | 28 | require('update-notifier')({ pkg }).notify() 29 | 30 | const cli = require('meow')(require('./help'), { 31 | pkg, 32 | description: false, 33 | flags: { 34 | concurrence: { 35 | alias: 'c', 36 | type: 'number', 37 | default: 8 38 | }, 39 | followRedirect: { 40 | alias: 'f', 41 | type: 'boolean', 42 | default: true 43 | }, 44 | logspeed: { 45 | type: 'number', 46 | default: 100 47 | }, 48 | prerender: { 49 | alias: 'p', 50 | default: 'auto' 51 | }, 52 | timeout: { 53 | alias: 't', 54 | type: 'number', 55 | default: 30000 56 | }, 57 | retries: { 58 | alias: 'r', 59 | type: 'number', 60 | default: 2 61 | } 62 | } 63 | }) 64 | 65 | const main = async () => { 66 | const { data: config = {} } = (await joycon.load()) || {} 67 | const input = config.url || cli.input 68 | 69 | if (isEmpty(input)) { 70 | cli.showHelp() 71 | await build.exit({ buildCode: 1, exitCode: 0 }) 72 | } 73 | 74 | const flags = { 75 | ...omit(config, ['url']), 76 | ...cli.flags 77 | } 78 | 79 | const url = getUrl(input) 80 | 81 | await build.start() 82 | const emitter = await metatags(url, flags) 83 | view({ emitter, ...flags }) 84 | } 85 | 86 | main().catch(async genericError => { 87 | console.error(beautyError(genericError)) 88 | await build.exit({ buildCode: 1, exitCode: 1 }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/cli/bin/color.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const pink = chalk.hex('#FF1493') 5 | 6 | module.exports = { 7 | pink, 8 | gray: chalk.gray, 9 | success: chalk.green, 10 | warning: chalk.yellow, 11 | error: chalk.red 12 | } 13 | -------------------------------------------------------------------------------- /packages/cli/bin/view/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { isNil } = require('lodash') 4 | const neatLog = require('neat-log') 5 | 6 | const build = require('../cli/build') 7 | const render = require('./render') 8 | 9 | module.exports = ({ total, emitter, logspeed, ...opts }) => { 10 | const state = { 11 | status: {}, 12 | count: 0, 13 | end: false, 14 | fetchingUrl: '', 15 | startTimestamp: Date.now(), 16 | exitCode: null 17 | } 18 | 19 | const neat = neatLog(render, { ...opts, logspeed, state }) 20 | 21 | let hasErrors = false 22 | 23 | neat.use((state, bus) => { 24 | emitter.on('urls', urls => (state.total = urls.length)) 25 | 26 | emitter.on('fetching', data => { 27 | state.fetchingUrl = data.url 28 | ++state.count 29 | }) 30 | 31 | emitter.on('fetched', ({ targetUrl, statusCode }) => { 32 | state.status[targetUrl] = statusCode 33 | }) 34 | 35 | emitter.on('rule', ({ status }) => { 36 | if (!hasErrors && status === 'error') { 37 | hasErrors = true 38 | } 39 | }) 40 | 41 | emitter.on('end', data => { 42 | state.data = data 43 | state.end = true 44 | state.exitCode = Number(hasErrors) 45 | }) 46 | 47 | setInterval(async () => { 48 | bus.emit('render') 49 | if (!isNil(state.exitCode)) { 50 | await build.exit({ buildCode: state.exitCode }) 51 | } 52 | }, logspeed) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /packages/cli/bin/view/render.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spinner = require('ora')({ text: '', color: 'gray' }) 4 | const indentString = require('indent-string') 5 | const cliTruncate = require('cli-truncate') 6 | const terminalSize = require('term-size') 7 | const logSymbols = require('log-symbols') 8 | const prettyMs = require('pretty-ms') 9 | 10 | const { STATUS_CODE } = require('http') 11 | const { EOL } = require('os') 12 | 13 | const color = require('../color') 14 | const { pink, gray } = color 15 | 16 | const { columns } = terminalSize() 17 | 18 | const truncateText = text => (text.length + 50 < columns ? text : cliTruncate(text, columns - 50)) 19 | 20 | const renderProgress = ({ fetchingUrl, count, total, startTimestamp }) => { 21 | const timestamp = pink(prettyMs(Date.now() - startTimestamp)) 22 | const spinnerFrame = spinner.frame() 23 | const url = gray(fetchingUrl) 24 | const progress = total ? gray(`${count} of ${total}`) : '' 25 | return `${EOL}${timestamp} ${spinnerFrame}${progress} ${url}${EOL}` 26 | } 27 | 28 | const renderResume = state => { 29 | return Object.keys(state.data).reduce((acc, url) => { 30 | const allRules = state.data[url] 31 | const statusCode = state.status[url] 32 | const humanStatusCode = gray(`${statusCode} ${STATUS_CODE[statusCode]}`) 33 | let str = `${url} ${humanStatusCode} ${EOL}` 34 | Object.keys(allRules).forEach(ruleName => { 35 | const rules = allRules[ruleName] 36 | str += `${EOL}${indentString(`${ruleName}`, 2)}${EOL}` 37 | 38 | rules.forEach(rule => { 39 | const colorize = color[rule.status] 40 | const value = truncateText(rule.value) 41 | const info = rule.message ? `${EOL}${indentString(`- ${rule.message}`, 2)}` : '' 42 | str += indentString( 43 | `${colorize(logSymbols[rule.status])} ${colorize(rule.selector)} ${gray( 44 | `(${rule.value.length})` 45 | )} ${gray(value)} ${colorize(info)}`, 46 | 4 47 | ) 48 | str += EOL 49 | }) 50 | }) 51 | return `${acc}${EOL}${str}` 52 | }, '') 53 | } 54 | 55 | module.exports = state => { 56 | const { quiet, end } = state 57 | if (quiet) return end ? renderResume(state) : '' 58 | return end ? renderResume(state) : renderProgress(state) 59 | } 60 | 61 | module.exports.resume = renderResume 62 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metatags", 3 | "description": "Ensure your HTML is previewed beautifully across social networks", 4 | "homepage": "https://github.com/microlinkhq/metatags#readme", 5 | "version": "1.0.1", 6 | "main": "bin/cli/index.js", 7 | "bin": { 8 | "metatags": "bin/cli/index.js" 9 | }, 10 | "repository": { 11 | "directory": "packages/cli", 12 | "type": "git", 13 | "url": "git+https://github.com/microlinkhq/metatags.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/microlinkhq/metatags/issues" 17 | }, 18 | "keywords": [ 19 | "cli", 20 | "facebook", 21 | "google", 22 | "html", 23 | "lint", 24 | "meta", 25 | "metatags", 26 | "og-image", 27 | "og:image", 28 | "opengraph", 29 | "seo", 30 | "tags", 31 | "twitter" 32 | ], 33 | "dependencies": { 34 | "@metatags/core": "^1.0.1", 35 | "beauty-error": "~1.2.8", 36 | "chalk": "~4.1.1", 37 | "ci-env": "~1.17.0", 38 | "cli-truncate": "~2.1.0", 39 | "github-build": "~1.2.2", 40 | "indent-string": "~4.0.0", 41 | "joycon": "~3.1.1", 42 | "lodash": "~4.17.21", 43 | "log-symbols": "~4.1.0", 44 | "meow": "~9.0.0", 45 | "neat-log": "~3.1.0", 46 | "ora": "~5.4.1", 47 | "prepend-http": "~3.0.1", 48 | "pretty-ms": "~7.0.1", 49 | "term-size": "~2.2.1", 50 | "update-notifier": "~5.1.0" 51 | }, 52 | "files": [ 53 | "bin" 54 | ], 55 | "scripts": { 56 | "test": "exit 0" 57 | }, 58 | "license": "MIT", 59 | "publishConfig": { 60 | "access": "public" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.0.1](https://github.com/microlinkhq/metatags/compare/v1.0.0...v1.0.1) (2021-11-12) 7 | 8 | **Note:** Version bump only for package @metatags/core 9 | 10 | 11 | 12 | 13 | 14 | # [1.0.0](https://github.com/microlinkhq/metatags/compare/v0.1.9...v1.0.0) (2021-09-14) 15 | 16 | **Note:** Version bump only for package @metatags/core 17 | 18 | 19 | 20 | 21 | 22 | ## [0.1.9](https://github.com/microlinkhq/metatags/compare/v0.1.8...v0.1.9) (2021-09-03) 23 | 24 | **Note:** Version bump only for package @metatags/core 25 | 26 | 27 | 28 | 29 | 30 | ## [0.1.3](https://github.com/microlinkhq/metatags/compare/v0.1.2...v0.1.3) (2021-09-02) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * lerna for public packages ([9f5c67d](https://github.com/microlinkhq/metatags/commit/9f5c67d70fc72ac7767fa6b59a3209f76645a157)) 36 | 37 | 38 | 39 | 40 | 41 | ## [0.1.2](https://github.com/microlinkhq/metatags/compare/v0.1.1...v0.1.2) (2021-09-02) 42 | 43 | **Note:** Version bump only for package @metatags/core 44 | 45 | 46 | 47 | 48 | 49 | ## [0.1.1](https://github.com/microlinkhq/metatags/compare/v0.1.0...v0.1.1) (2021-07-23) 50 | 51 | **Note:** Version bump only for package @metatags/core 52 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metatags/core", 3 | "description": "Ensure your HTML is previewed beauty across social networks.", 4 | "homepage": "https://github.com/microlinkhq/metatags#readme", 5 | "version": "1.0.1", 6 | "main": "src/index.js", 7 | "repository": { 8 | "directory": "packages/core", 9 | "type": "git", 10 | "url": "git+https://github.com/microlinkhq/metatags.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/microlinkhq/metatags/issues" 14 | }, 15 | "keywords": [ 16 | "facebook", 17 | "google", 18 | "html", 19 | "lint", 20 | "meta", 21 | "metatags", 22 | "og-image", 23 | "og:image", 24 | "opengraph", 25 | "seo", 26 | "tags", 27 | "twitter" 28 | ], 29 | "dependencies": { 30 | "aigle": "~1.14.1", 31 | "cheerio": "~1.1.0", 32 | "html-get": "~2.21.1", 33 | "is-url-http": "~2.3.0", 34 | "mitt": "~3.0.0", 35 | "reachable-url": "~1.8.0", 36 | "require-one-of": "~1.0.15", 37 | "signal-exit": "~4.1.0", 38 | "whoops": "~5.0.1", 39 | "xml-urls": "~2.1.25" 40 | }, 41 | "files": [ 42 | "src" 43 | ], 44 | "scripts": { 45 | "test": "exit 0" 46 | }, 47 | "license": "MIT", 48 | "publishConfig": { 49 | "access": "public" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/src/get-browserless.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const creatBrowserlessFactory = opts => { 4 | const requireOneOf = require('require-one-of') 5 | const createBrowserless = requireOneOf(['@browserless/pool', 'browserless']) 6 | const { onExit } = require('signal-exit') 7 | 8 | const browserlessFactory = createBrowserless(opts) 9 | onExit(browserlessFactory.close) 10 | return browserlessFactory 11 | } 12 | 13 | let _browserlessFactory = null 14 | 15 | module.exports = opts => 16 | _browserlessFactory || (_browserlessFactory = creatBrowserlessFactory(opts)) 17 | -------------------------------------------------------------------------------- /packages/core/src/get-urls.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fromXML = require('xml-urls') 4 | const aigle = require('aigle') 5 | 6 | const { isXmlUrl } = fromXML 7 | 8 | module.exports = async (urls, opts) => { 9 | const collection = [...new Set([].concat(urls))] 10 | 11 | const iterator = async (set, url) => { 12 | const urls = isXmlUrl(url) ? await fromXML(url, opts) : [url] 13 | return new Set([...set, ...urls]) 14 | } 15 | 16 | const set = await aigle.reduce(collection, iterator, new Set()) 17 | const result = Array.from(set) 18 | return result 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // const timeSpan = require('time-span') 4 | 5 | const getHTML = require('html-get') 6 | const cheerio = require('cheerio') 7 | const aigle = require('aigle') 8 | const mitt = require('mitt') 9 | 10 | const _getBrowserless = require('./get-browserless') 11 | const getUrls = require('./get-urls') 12 | const allRules = require('./rules') 13 | 14 | const evaluateRule = async ({ value, validator, el }) => { 15 | let status = 'success' 16 | let message 17 | 18 | try { 19 | await validator({ value, el }) 20 | } catch (error) { 21 | status = error.status 22 | message = error.message 23 | } 24 | 25 | return { status, message } 26 | } 27 | 28 | const validate = async (html, emitter) => { 29 | const $ = cheerio.load(html) 30 | 31 | return aigle.reduce( 32 | allRules, 33 | async (acc, rules, rulesName) => { 34 | const evaluatedRules = await aigle.map(rules, async rule => { 35 | const { selector, attr } = rule 36 | const el = $(`head ${selector}`) 37 | const value = (attr ? el.attr(attr) : el.text()) || '' 38 | 39 | const result = await evaluateRule({ ...rule, value, el }) 40 | const evaluatedRule = { ...rule, ...result, value } 41 | 42 | emitter.emit('rule', evaluatedRule) 43 | return evaluatedRule 44 | }) 45 | 46 | acc[rulesName] = evaluatedRules 47 | return acc 48 | }, 49 | {} 50 | ) 51 | } 52 | 53 | const validateUrl = async ({ acc, url, emitter, ...opts }) => { 54 | emitter.emit('fetching', { url }) 55 | const data = await getHTML(url, opts) 56 | emitter.emit('fetched', { ...data, targetUrl: url }) 57 | 58 | const report = await validate(data.html, emitter) 59 | emitter.emit('report', report) 60 | 61 | acc[url] = report 62 | } 63 | 64 | const resolveUrls = async (urls, { emitter, concurrence, ...opts } = {}) => 65 | aigle.transformLimit( 66 | urls, 67 | concurrence, 68 | (acc, url) => validateUrl({ acc, url, emitter, ...opts }), 69 | {} 70 | ) 71 | 72 | module.exports = ( 73 | urls, 74 | { emitter = mitt(), concurrence = 8, getBrowserless = _getBrowserless, ...opts } = {} 75 | ) => { 76 | getUrls(urls, { getBrowserless, ...opts }) 77 | .then(urls => { 78 | emitter.emit('urls', urls) 79 | return resolveUrls(urls, { emitter, concurrence, getBrowserless, ...opts }) 80 | }) 81 | .then(data => emitter.emit('end', data)) 82 | .catch(error => emitter.emit('error', error)) 83 | 84 | return emitter 85 | } 86 | -------------------------------------------------------------------------------- /packages/core/src/rules.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const VALIDATOR = require('./validators') 4 | 5 | module.exports = { 6 | // Search Engine 7 | basic: [ 8 | { selector: 'title', validator: VALIDATOR.title }, 9 | { 10 | selector: 'link[rel="icon" i], link[rel="shortcut icon" i]', 11 | attr: 'href', 12 | validator: VALIDATOR.presence 13 | }, 14 | { 15 | selector: 'meta[name="description"]', 16 | attr: 'content', 17 | validator: VALIDATOR.description 18 | }, 19 | { 20 | selector: 'meta[charset]', 21 | attr: 'charset', 22 | validator: VALIDATOR.presence 23 | }, 24 | { 25 | selector: 'meta[name="author"]', 26 | attr: 'content', 27 | validator: VALIDATOR.notEmpty 28 | }, 29 | { selector: 'link[rel="canonical"]', attr: 'href', validator: VALIDATOR.url }, 30 | { selector: 'meta[name="viewport"]', attr: 'content', validator: VALIDATOR.notEmpty } 31 | ], 32 | // Schema.org for Google 33 | google: [ 34 | { selector: 'meta[itemprop="name"]', attr: 'content', validator: VALIDATOR.title }, 35 | { 36 | selector: '[itemprop*="author" i] [itemprop="name"], [itemprop*="author" i]', 37 | validator: VALIDATOR.notEmpty 38 | }, 39 | { 40 | selector: 'meta[itemprop="description"]', 41 | attr: 'content', 42 | validator: VALIDATOR.description 43 | }, 44 | { selector: 'meta[itemprop="image"]', attr: 'content', validator: VALIDATOR.url } 45 | ], 46 | twitter: [ 47 | { selector: 'meta[name="twitter:card"]', attr: 'content', validator: VALIDATOR.notEmpty }, 48 | { selector: 'meta[name="twitter:title"]', attr: 'content', validator: VALIDATOR.title }, 49 | { 50 | selector: 'meta[name="twitter:description"]', 51 | attr: 'content', 52 | validator: VALIDATOR.description 53 | }, 54 | { selector: 'meta[name="twitter:image"]', attr: 'content', validator: VALIDATOR.url }, 55 | { selector: 'meta[name="twitter:image:alt"]', attr: 'content', validator: VALIDATOR.notEmpty }, 56 | { selector: 'meta[name="twitter:site"]', attr: 'content', validator: VALIDATOR.notEmpty }, 57 | { selector: 'meta[name="twitter:creator"]', attr: 'content', validator: VALIDATOR.notEmpty } 58 | ], 59 | // Open Graph general (Facebook, Pinterest & Google+) 60 | facebook: [ 61 | { selector: 'meta[property="og:title"]', attr: 'content', validator: VALIDATOR.title }, 62 | { 63 | selector: 'meta[property="og:description"]', 64 | attr: 'content', 65 | validator: VALIDATOR.notEmpty 66 | }, 67 | { selector: 'meta[property="og:image"]', attr: 'content', validator: VALIDATOR.url }, 68 | { selector: 'meta[property="og:image:alt"]', attr: 'content', validator: VALIDATOR.notEmpty }, 69 | { selector: 'meta[property="og:logo"]', attr: 'content', validator: VALIDATOR.url }, 70 | { selector: 'meta[property="og:url"]', attr: 'content', validator: VALIDATOR.url }, 71 | { selector: 'meta[property="og:type"]', attr: 'content', validator: VALIDATOR.notEmpty }, 72 | { selector: 'meta[property="og:site_name"]', attr: 'content', validator: VALIDATOR.notEmpty } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /packages/core/src/validators.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const reachableUrl = require('reachable-url') 4 | const isUrl = require('is-url-http') 5 | const whoops = require('whoops') 6 | 7 | const inRange = (num, init, final) => num >= Math.min(init, final) && num < Math.max(init, final) 8 | 9 | const ruleError = whoops('RuleError', { status: 'error' }) 10 | ruleError.warning = props => ruleError({ ...props, status: 'warning' }) 11 | 12 | const VALIDATOR = { 13 | url: async ({ value }) => { 14 | if (value.toString().length === 0) { 15 | throw ruleError({ message: 'Expected to be present' }) 16 | } 17 | 18 | if (!isUrl(value)) { 19 | throw ruleError({ message: `Expected an absolute WHATWG URL, got \`${value}\`` }) 20 | } 21 | 22 | const res = await reachableUrl(value) 23 | 24 | if (!reachableUrl.isReachable(res)) { 25 | throw ruleError({ 26 | message: `Expected a reachable URL, got ${res.statusCode} HTTP status code` 27 | }) 28 | } 29 | }, 30 | /* when is present, it can't be empty */ 31 | notEmpty: ({ el, value }) => { 32 | if (el.length !== 0 && value.toString().length === 0) { 33 | throw ruleError.warning({ 34 | message: 'Expected to be not empty' 35 | }) 36 | } 37 | }, 38 | /** it should be exist and no be empty */ 39 | presence: ({ el, value }) => { 40 | if (el.length === 0) { 41 | throw ruleError({ message: 'Expected to be present' }) 42 | } 43 | if (value.toString().length === 0) { 44 | throw ruleError.warning({ 45 | message: 'Expected to be not empty' 46 | }) 47 | } 48 | }, 49 | title: ({ el, value }) => { 50 | if (el.length === 0) { 51 | throw ruleError({ 52 | message: 'Expected to be present', 53 | link: 'https://moz.com/learn/seo/title-tag' 54 | }) 55 | } 56 | 57 | if (!inRange(value.toString().length, 50, 60)) { 58 | throw ruleError.warning({ 59 | message: 'Recommended a value between 50 and 60 characters', 60 | link: 'https://moz.com/learn/seo/title-tag' 61 | }) 62 | } 63 | }, 64 | description: ({ value }) => { 65 | if (value.toString().length === 0) { 66 | throw ruleError({ message: 'Expected to be present' }) 67 | } 68 | 69 | if (!inRange(value.toString().length, 50, 160)) { 70 | throw ruleError.warning({ 71 | message: 'Recommended a value between 50 and 160 characters', 72 | link: 'https://moz.com/learn/seo/meta-description' 73 | }) 74 | } 75 | } 76 | } 77 | 78 | module.exports = VALIDATOR 79 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /static/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/metatags/5108da08e8312f085507b122621f224cfcda96b1/static/banner.png -------------------------------------------------------------------------------- /static/banner.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/metatags/5108da08e8312f085507b122621f224cfcda96b1/static/banner.sketch -------------------------------------------------------------------------------- /static/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microlinkhq/metatags/5108da08e8312f085507b122621f224cfcda96b1/static/demo.png --------------------------------------------------------------------------------