├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── favicon.js ├── index.js └── util.js └── test ├── index.js ├── mocha.opts └── setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | "__SERVER__": true, 8 | "__CLIENT__": true, 9 | "__TEST__": true, 10 | "__DEV__": true, 11 | "__PROD__": true, 12 | "__STAGING__": true 13 | }, 14 | "ecmaFeatures": { 15 | "arrowFunctions": true, 16 | "binaryLiterals": true, 17 | "blockBindings": true, 18 | "classes": true, 19 | "defaultParams": true, 20 | "destructuring": true, 21 | "forOf": true, 22 | "generators": true, 23 | "modules": true, 24 | "objectLiteralComputedProperties": true, 25 | "objectLiteralShorthandMethods": true, 26 | "objectLiteralShorthandProperties": true, 27 | "octalLiterals": true, 28 | "regexUFlag": true, 29 | "regexYFlag": true, 30 | "spread": true, 31 | "superInFunctions": true, 32 | "templateStrings": true, 33 | "unicodeCodePointEscapes": true, 34 | "jsx": true 35 | }, 36 | "parser": "babel-eslint", 37 | "rules": { 38 | "prefer-reflect": [ 39 | 0, 40 | { 41 | "exceptions": [ 42 | "apply", 43 | "call", 44 | "delete" 45 | ] 46 | } 47 | ], 48 | "babel/new-cap": 2, 49 | "no-return-assign": 2, 50 | "no-invalid-this": 0, 51 | "no-void": 2, 52 | "one-var": [2, "never"], 53 | "react/jsx-closing-bracket-location": 0, 54 | "no-undef": 2, 55 | "max-nested-callbacks": [ 56 | 2, 57 | 3 58 | ], 59 | "no-empty": 2, 60 | "no-loop-func": 2, 61 | "keyword-spacing": 2, 62 | "babel/object-shorthand": [ 63 | 2, 64 | "always" 65 | ], 66 | "wrap-iife": [ 67 | 2, 68 | "inside" 69 | ], 70 | "valid-typeof": 2, 71 | "react/jsx-no-literals": 2, 72 | "handle-callback-err": 2, 73 | "operator-linebreak": [2, "after"], 74 | "no-label-var": 2, 75 | "no-process-env": 2, 76 | "no-irregular-whitespace": 2, 77 | "block-spacing": 2, 78 | "padded-blocks": [ 79 | 2, 80 | "never" 81 | ], 82 | "react/jsx-pascal-case": 2, 83 | "no-empty-pattern": 2, 84 | "radix": 2, 85 | "no-undefined": 0, 86 | "semi-spacing": 2, 87 | "eqeqeq": [ 88 | 2, 89 | "allow-null" 90 | ], 91 | "no-negated-condition": 2, 92 | "require-yield": 2, 93 | "new-cap": 2, 94 | "no-const-assign": 2, 95 | "no-bitwise": 2, 96 | "dot-notation": 2, 97 | "camelcase": 2, 98 | "prefer-const": 2, 99 | "no-negated-in-lhs": 2, 100 | "prefer-arrow-callback": 2, 101 | "no-extra-bind": 2, 102 | "react/prefer-es6-class": 2, 103 | "no-sequences": 2, 104 | "babel/generator-star-spacing": 2, 105 | "comma-dangle": [ 106 | 2, 107 | "always-multiline" 108 | ], 109 | "no-spaced-func": 2, 110 | "react/require-extension": 2, 111 | "no-labels": 2, 112 | "no-unreachable": 2, 113 | "no-eval": 2, 114 | "react/no-did-mount-set-state": 2, 115 | "no-unneeded-ternary": 2, 116 | "no-process-exit": 2, 117 | "no-empty-character-class": 2, 118 | "constructor-super": 2, 119 | "no-dupe-class-members": 2, 120 | "strict": [ 121 | 2, 122 | "never" 123 | ], 124 | "no-case-declarations": 2, 125 | "array-bracket-spacing": 2, 126 | "react/no-set-state": 2, 127 | "block-scoped-var": 2, 128 | "arrow-body-style": 2, 129 | "space-in-parens": 2, 130 | "no-confusing-arrow": 2, 131 | "no-control-regex": 2, 132 | "consistent-return": 2, 133 | "no-console": 2, 134 | "comma-spacing": 2, 135 | "no-redeclare": 2, 136 | "computed-property-spacing": 2, 137 | "no-invalid-regexp": 2, 138 | "use-isnan": 2, 139 | "no-new-require": 2, 140 | "indent": [ 141 | 2, 142 | 2 143 | ], 144 | "react/react-in-jsx-scope": 2, 145 | "no-native-reassign": 2, 146 | "no-func-assign": 2, 147 | "max-len": [ 148 | 2, 149 | 120, 150 | 4, 151 | { 152 | "ignoreUrls": true 153 | } 154 | ], 155 | "no-shadow": [ 156 | 2, 157 | { 158 | "builtinGlobals": true 159 | } 160 | ], 161 | "no-mixed-requires": 2, 162 | "react/no-did-update-set-state": 2, 163 | "react/jsx-uses-react": 2, 164 | "max-statements": [ 165 | 2, 166 | 20 167 | ], 168 | "space-unary-ops": [ 169 | 2, 170 | { 171 | "words": true, 172 | "nonwords": false 173 | } 174 | ], 175 | "no-lone-blocks": 2, 176 | "no-debugger": 2, 177 | "arrow-parens": [ 178 | 2, 179 | "always" 180 | ], 181 | "space-before-blocks": [ 182 | 2, 183 | "always" 184 | ], 185 | "no-implied-eval": 2, 186 | "no-useless-concat": 2, 187 | "no-multi-spaces": 2, 188 | "curly": [2, "multi-line"], 189 | "no-extra-boolean-cast": 2, 190 | "space-infix-ops": 2, 191 | "babel/no-await-in-loop": 2, 192 | "react/sort-comp": 2, 193 | "react/jsx-no-undef": 2, 194 | "no-multiple-empty-lines": [ 195 | 2, 196 | { 197 | "max": 2 198 | } 199 | ], 200 | "semi": 2, 201 | "no-param-reassign": 0, 202 | "no-cond-assign": 2, 203 | "no-dupe-keys": 2, 204 | "import/named": 0, 205 | "max-params": [ 206 | 2, 207 | 4 208 | ], 209 | "linebreak-style": 2, 210 | "react/jsx-sort-props": [ 211 | 0, 212 | { 213 | "shorthandFirst": true, 214 | "callbacksLast": true 215 | } 216 | ], 217 | "no-octal-escape": 2, 218 | "no-this-before-super": 2, 219 | "no-alert": 2, 220 | "react/jsx-no-duplicate-props": [ 221 | 2, 222 | { 223 | "ignoreCase": true 224 | } 225 | ], 226 | "no-unused-expressions": 2, 227 | "react/jsx-sort-prop-types": 0, 228 | "no-class-assign": 2, 229 | "spaced-comment": 2, 230 | "no-path-concat": 2, 231 | "prefer-spread": 2, 232 | "no-self-compare": 2, 233 | "guard-for-in": 2, 234 | "no-nested-ternary": 2, 235 | "no-multi-str": 2, 236 | "react/jsx-key": 1, 237 | "import/namespace": 2, 238 | "no-warning-comments": 1, 239 | "no-delete-var": 2, 240 | "babel/arrow-parens": [ 241 | 2, 242 | "always" 243 | ], 244 | "no-with": 2, 245 | "no-extra-parens": 2, 246 | "no-trailing-spaces": 2, 247 | "import/no-unresolved": 1, 248 | "no-obj-calls": 2, 249 | "accessor-pairs": 2, 250 | "yoda": [ 251 | 2, 252 | "never", 253 | { 254 | "exceptRange": true 255 | } 256 | ], 257 | "no-continue": 1, 258 | "react/no-unknown-property": 2, 259 | "no-new": 2, 260 | "object-curly-spacing": 2, 261 | "react/jsx-curly-spacing": [ 262 | 2, 263 | "never" 264 | ], 265 | "jsx-quotes": 2, 266 | "react/no-direct-mutation-state": 2, 267 | "key-spacing": 2, 268 | "no-underscore-dangle": [ 269 | 2, 270 | { "allowAfterThis": true } 271 | ], 272 | "new-parens": 2, 273 | "no-mixed-spaces-and-tabs": 2, 274 | "no-floating-decimal": 2, 275 | "operator-assignment": [ 276 | 2, 277 | "always" 278 | ], 279 | "no-shadow-restricted-names": 2, 280 | "no-use-before-define": [ 281 | 2, 282 | "nofunc" 283 | ], 284 | "no-useless-call": 2, 285 | "no-caller": 2, 286 | "quotes": [ 287 | 2, 288 | "single", 289 | "avoid-escape" 290 | ], 291 | "react/jsx-handler-names": [ 292 | 1, 293 | { 294 | "eventHandlerPrefix": "handle", 295 | "eventHandlerPropPrefix": "on" 296 | } 297 | ], 298 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 299 | "no-unused-vars": 2, 300 | "import/default": 1, 301 | "no-lonely-if": 2, 302 | "no-extra-semi": 2, 303 | "prefer-template": 2, 304 | "react/forbid-prop-types": 1, 305 | "react/self-closing-comp": 2, 306 | "no-else-return": 2, 307 | "react/jsx-max-props-per-line": [ 308 | 2, 309 | { 310 | "maximum": 3 311 | } 312 | ], 313 | "no-dupe-args": 2, 314 | "no-new-object": 2, 315 | "callback-return": 2, 316 | "no-new-wrappers": 2, 317 | "comma-style": 2, 318 | "no-script-url": 2, 319 | "consistent-this": 2, 320 | "react/wrap-multilines": 0, 321 | "dot-location": [ 322 | 2, 323 | "property" 324 | ], 325 | "no-implicit-coercion": 2, 326 | "max-depth": [ 327 | 2, 328 | 4 329 | ], 330 | "babel/object-curly-spacing": [ 331 | 2, 332 | "never" 333 | ], 334 | "no-array-constructor": 2, 335 | "no-iterator": 2, 336 | "react/jsx-no-bind": 2, 337 | "sort-vars": 2, 338 | "no-var": 2, 339 | "no-sparse-arrays": 2, 340 | "space-before-function-paren": [ 341 | 2, 342 | "never" 343 | ], 344 | "no-throw-literal": 2, 345 | "no-proto": 2, 346 | "default-case": 2, 347 | "no-inner-declarations": 2, 348 | "react/jsx-indent-props": [ 349 | 2, 350 | 2 351 | ], 352 | "no-new-func": 2, 353 | "object-shorthand": 2, 354 | "no-ex-assign": 2, 355 | "no-unexpected-multiline": 2, 356 | "no-undef-init": 2, 357 | "no-duplicate-case": 2, 358 | "no-fallthrough": 2, 359 | "no-catch-shadow": 2, 360 | "import/export": 2, 361 | "no-constant-condition": 2, 362 | "complexity": [ 363 | 2, 364 | 25 365 | ], 366 | "react/jsx-boolean-value": [ 367 | 2, 368 | "never" 369 | ], 370 | "valid-jsdoc": 2, 371 | "no-extend-native": 2, 372 | "react/prop-types": 2, 373 | "no-regex-spaces": 2, 374 | "react/no-multi-comp": 2, 375 | "no-octal": 2, 376 | "arrow-spacing": 2, 377 | "quote-props": [ 378 | 2, 379 | "as-needed" 380 | ], 381 | "no-div-regex": 2, 382 | "react/jsx-uses-vars": 2, 383 | "react/no-danger": 1 384 | }, 385 | "settings": { 386 | "ecmascript": 6, 387 | "jsx": true, 388 | "import/parser": "babel-eslint", 389 | "import/ignore": [ 390 | "node_modules", 391 | "\\.scss$" 392 | ], 393 | "import/resolve": { 394 | "moduleDirectory": [ 395 | "node_modules" 396 | ] 397 | } 398 | }, 399 | "plugins": [ 400 | "react", 401 | "import", 402 | "babel" 403 | ] 404 | } 405 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Compiled source 40 | lib 41 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc 3 | .gitignore 4 | .npmignore 5 | .nyc_output 6 | .travis.yml 7 | coverage 8 | src 9 | test 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "5.1" 5 | - "4" 6 | - "4.2" 7 | - "4.1" 8 | - "4.0" 9 | - "0.12" 10 | - "0.11" 11 | - "0.10" 12 | - "iojs" 13 | after_success: npm run coverage 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joon Ho Cho 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-hat 2 | [![Build Status](https://travis-ci.org/joonhocho/react-hat.svg?branch=master)](https://travis-ci.org/joonhocho/react-hat) 3 | [![Coverage Status](https://coveralls.io/repos/github/joonhocho/react-hat/badge.svg?branch=master)](https://coveralls.io/github/joonhocho/react-hat?branch=master) 4 | [![npm version](https://badge.fury.io/js/react-hat.svg)](https://badge.fury.io/js/react-hat) 5 | [![Dependency Status](https://david-dm.org/joonhocho/react-hat.svg)](https://david-dm.org/joonhocho/react-hat) 6 | [![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org) 7 | 8 | A higher order component for react-helmet that auto fills favicon, og, twitter meta tags for SEO. 9 | 10 | Checkout [react-hat](https://github.com/joonhocho/react-hat) for validating string inputs. 11 | 12 | 13 | ### Install 14 | ``` 15 | npm install --save react-hat 16 | ``` 17 | 18 | 19 | ### Usage 20 | ```javascript 21 | import React from "react"; 22 | import Hat from "react-hat"; 23 | 24 |
25 | 26 | ... 27 |
28 | ``` 29 | 30 | 31 | ### Credits 32 | [react-helmet](https://github.com/nfl/react-helmet) 33 | 34 | 35 | ### License 36 | ``` 37 | The MIT License (MIT) 38 | 39 | Copyright (c) 2016 Joon Ho Cho 40 | 41 | Permission is hereby granted, free of charge, to any person obtaining a copy 42 | of this software and associated documentation files (the "Software"), to deal 43 | in the Software without restriction, including without limitation the rights 44 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 45 | copies of the Software, and to permit persons to whom the Software is 46 | furnished to do so, subject to the following conditions: 47 | 48 | The above copyright notice and this permission notice shall be included in all 49 | copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 52 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 53 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 54 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 55 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 56 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 57 | SOFTWARE. 58 | ``` 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hat", 3 | "version": "0.0.5", 4 | "description": "A higher order component for react-helmet that automatically fills og / twitter meta tags", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "build-watch": "babel src --watch --out-dir lib", 9 | "clear": "rm -rf ./lib ./coverage ./.nyc_output", 10 | "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 11 | "nyc": "nyc npm test && nyc report --reporter=lcov", 12 | "pretest": "npm run build", 13 | "start": "npm test", 14 | "test": "mocha", 15 | "test-watch": "mocha --watch", 16 | "update-D": "npm install --save-dev babel-cli@latest babel-preset-es2015@latest babel-preset-stage-0@latest babel-register@latest chai@latest chai-as-promised@latest coveralls@latest mocha@latest nyc@latest", 17 | "watch": "npm run build-watch & npm run test-watch" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/joonhocho/react-hat.git" 22 | }, 23 | "keywords": [ 24 | "react-helmet", 25 | "wrapper", 26 | "higher", 27 | "meta", 28 | "og", 29 | "open graph", 30 | "twitter" 31 | ], 32 | "author": "Joon Ho Cho", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/joonhocho/react-hat/issues" 36 | }, 37 | "homepage": "https://github.com/joonhocho/react-hat#readme", 38 | "dependencies": { 39 | "proptypes-parser": "^0.1.1", 40 | "react-helmet": "3.x.x" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "^6.9.0", 44 | "babel-preset-es2015": "^6.9.0", 45 | "babel-preset-react": "^6.11.1", 46 | "babel-preset-stage-0": "^6.5.0", 47 | "babel-register": "^6.9.0", 48 | "chai": "^3.5.0", 49 | "chai-as-promised": "^5.3.0", 50 | "coveralls": "^2.11.9", 51 | "mocha": "^2.5.3", 52 | "nyc": "^6.4.4", 53 | "react": "^15.2.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/favicon.js: -------------------------------------------------------------------------------- 1 | const appleIconRel = 'apple-touch-icon-precomposed'; 2 | const pngType = 'image/png'; 3 | 4 | export function iconLink(href, size, type = pngType) { 5 | return {rel: 'icon', type, href, sizes: `${size}x${size}`}; 6 | } 7 | 8 | export function appleIconLink(href, size) { 9 | return {rel: appleIconRel, href, sizes: `${size}x${size}`}; 10 | } 11 | 12 | export function msSquare(content, size) { 13 | return {name: `msapplication-square${size}x${size}logo`, content}; 14 | } 15 | 16 | export default function addFavicon({ 17 | ico, 18 | s16, 19 | s32, 20 | s57, 21 | s60, 22 | s70, 23 | s72, 24 | s76, 25 | s96, 26 | s114, 27 | s120, 28 | s128, 29 | s144, 30 | s150, 31 | s152, 32 | s196, 33 | s310, 34 | s310x150, 35 | }, meta, link) { 36 | if (ico) link.push({rel: 'shortcut icon', href: ico}); 37 | // 38 | 39 | if (s16) link.push(iconLink(s16, 16)); 40 | if (s32) link.push(iconLink(s32, 32)); 41 | if (s96) link.push(iconLink(s96, 96)); 42 | if (s128) link.push(iconLink(s128, 128)); 43 | if (s196) link.push(iconLink(s196, 196)); 44 | 45 | if (s57) link.push(appleIconLink(s57, 57)); 46 | if (s60) link.push(appleIconLink(s60, 60)); 47 | if (s72) link.push(appleIconLink(s72, 72)); 48 | if (s76) link.push(appleIconLink(s76, 76)); 49 | if (s114) link.push(appleIconLink(s114, 114)); 50 | if (s120) link.push(appleIconLink(s120, 120)); 51 | if (s144) link.push(appleIconLink(s144, 144)); 52 | if (s152) link.push(appleIconLink(s152, 152)); 53 | 54 | // TODO 55 | if (s144) meta.push({name: 'msapplication-TileImage', content: s144}); 56 | 57 | if (s70) meta.push(msSquare(s70, 70)); 58 | if (s150) meta.push(msSquare(s150, 150)); 59 | if (s310) meta.push(msSquare(s310, 310)); 60 | 61 | if (s310x150) meta.push({name: 'msapplication-wide310x150logo', content: s310x150}); 62 | } 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import {PT} from 'proptypes-parser'; 4 | import {forEach, truncate, findIndex} from './util'; 5 | import addFavicon from './favicon'; 6 | 7 | const twitterTitleMaxLen = 70; 8 | const twitterDescMaxLen = 200; 9 | 10 | const hasObjectItem = (arr, name, value) => 11 | findIndex(arr, ({[name]: val}) => val === value) >= 0; 12 | 13 | const toHelmetProps = ({ 14 | base, 15 | defaultTitle, 16 | description, 17 | favicon, 18 | image, 19 | link, 20 | meta, 21 | og, 22 | property, 23 | siteName, 24 | title, 25 | titleTemplate, 26 | twitter, 27 | url, 28 | ...rest, 29 | }) => { 30 | link = link || []; 31 | meta = meta || []; 32 | og = og || {}; 33 | twitter = twitter || {}; 34 | 35 | // TODO: itemProp 36 | 37 | // siteName 38 | if (siteName != null) { 39 | if (defaultTitle == null) defaultTitle = siteName; 40 | if (titleTemplate === true) titleTemplate = `%s | ${siteName}`; 41 | if (og.site_name == null) og.site_name = siteName; 42 | if (!hasObjectItem(meta, 'name', 'application-name')) { 43 | meta.push({name: 'application-name', content: siteName}); 44 | } 45 | } 46 | 47 | 48 | // favicon 49 | if (favicon) { 50 | addFavicon(favicon, meta, link); 51 | } 52 | 53 | 54 | // title 55 | if (title != null) { 56 | if (og.title == null) og.title = title; 57 | if (twitter.title == null) twitter.title = title; 58 | } 59 | if (twitter.title) { 60 | twitter.title = truncate(twitter.title, twitterTitleMaxLen, '...'); 61 | } 62 | 63 | 64 | // description 65 | if (description != null) { 66 | if (og.description == null) og.description = description; 67 | if (twitter.description == null) twitter.description = description; 68 | if (!hasObjectItem(meta, 'name', 'description')) { 69 | meta.push({name: 'description', content: description}); 70 | } 71 | } 72 | if (twitter.description) { 73 | twitter.description = truncate(twitter.description, twitterDescMaxLen, '...'); 74 | } 75 | 76 | 77 | // image 78 | if (image != null) { 79 | if (og.image == null) og.image = image; 80 | if (twitter.image == null) twitter.image = image; 81 | if (!hasObjectItem(link, 'rel', 'apple-touch-startup-image')) { 82 | link.push({rel: 'apple-touch-startup-image', href: image}); 83 | } 84 | } 85 | 86 | 87 | // url 88 | if (url != null) { 89 | if (og.url == null) og.url = url; 90 | if (twitter.url == null) twitter.url = url; 91 | if (!hasObjectItem(link, 'rel', 'canonical')) { 92 | link.push({rel: 'canonical', href: url}); 93 | } 94 | } 95 | 96 | 97 | // og 98 | forEach(og, (content, prop) => content != null && 99 | meta.push({property: `og:${prop}`, content})); 100 | 101 | 102 | // twitter 103 | forEach(twitter, (content, prop) => content != null && 104 | meta.push({property: `twitter:${prop}`, content})); 105 | 106 | 107 | // property 108 | if (property) { 109 | forEach(property, (props, namespace) => 110 | forEach(props, (content, prop) => content != null && 111 | meta.push({property: `${namespace}:${prop}`, content}) 112 | ) 113 | ); 114 | } 115 | 116 | 117 | // rest 118 | if (base != null) rest.base = typeof base === 'string' ? {href: base} : base; 119 | if (defaultTitle != null) rest.defaultTitle = defaultTitle; 120 | if (link.length) rest.link = link; 121 | if (meta.length) rest.meta = meta; 122 | if (title != null) rest.title = title; 123 | if (titleTemplate != null) rest.titleTemplate = titleTemplate; 124 | 125 | /** 126 | * @param {Object} htmlAttributes: {"lang": "en", "amp": undefined} 127 | * @param {String} title: "Title" 128 | * @param {String} defaultTitle: "Default Title" 129 | * @param {String} titleTemplate: "MySite.com - %s" 130 | * @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"} 131 | * @param {Array} meta: [{"name": "description", "content": "Test description"}] 132 | * @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}] 133 | * @param {Array} script: [{"type": "text/javascript", "src": "http://mysite.com/js/test.js"}] 134 | * @param {Array} style: [{"type": "text/css", "cssText": "div{ display: block; color: blue; }"}] 135 | * @param {Function} onChangeClientState: "(newState) => console.log(newState)" 136 | */ 137 | return rest; 138 | }; 139 | 140 | 141 | const Hat = (props) => ; 142 | 143 | Hat.propTypes = PT`{ 144 | base: String | Object 145 | defaultTitle: String 146 | description: String 147 | favicon: Object 148 | image: String 149 | link: [Object!] 150 | meta: [Object!] 151 | og: Object 152 | property: Object 153 | siteName: String 154 | title: String 155 | titleTemplate: String | Boolean 156 | twitter: Object 157 | url: String 158 | }`; 159 | 160 | 161 | export { 162 | toHelmetProps, 163 | Hat, 164 | }; 165 | 166 | export default Hat; 167 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | function forEach(obj, fn) { 2 | const hasOwn = Object.hasOwnProperty; 3 | for (const key in obj) { 4 | if (hasOwn.call(obj, key)) { 5 | fn(obj[key], key, obj); 6 | } 7 | } 8 | } 9 | 10 | function truncate(str, maxLength, suffix = '') { 11 | if (str.length > maxLength) { 12 | return str.substring(0, maxLength - suffix.length) + suffix; 13 | } 14 | return str; 15 | } 16 | 17 | function findIndex(arr, fn) { 18 | for (let i = 0; i < arr.length; i++) { 19 | if (fn(arr[i])) { 20 | return i; 21 | } 22 | } 23 | return -1; 24 | } 25 | 26 | 27 | export { 28 | forEach, 29 | truncate, 30 | findIndex, 31 | }; 32 | 33 | export default { 34 | forEach, 35 | truncate, 36 | findIndex, 37 | }; 38 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'mocha'; 2 | import {expect} from 'chai'; 3 | import { 4 | toHelmetProps, 5 | Hat, 6 | } from '../lib'; 7 | 8 | 9 | describe('toHelmetProps', () => { 10 | it('adds og and twitter props', () => { 11 | const props = toHelmetProps({ 12 | base: 'http://example.com', 13 | description: 'description text', 14 | favicon: { 15 | ico: '/favicon.ico', 16 | s16: '/favicon-16.png', 17 | s70: '/favicon-70.png', 18 | s144: '/favicon-144.png', 19 | }, 20 | image: '/image.jpg', 21 | link: [ 22 | {rel: 'link rel', href: 'link href'}, 23 | {rel: 'canonical', href: '/canonical'}, 24 | ], 25 | meta: [{name: 'meta name', content: 'meta content'}], 26 | og: { 27 | k1: 'v1', 28 | }, 29 | property: { 30 | ns: { 31 | k2: 'v2', 32 | }, 33 | }, 34 | siteName: 'site name', 35 | title: 'page title', 36 | twitter: { 37 | k3: 'v3', 38 | }, 39 | url: '/page.url', 40 | }); 41 | 42 | expect(props).to.eql({ 43 | base: {href: 'http://example.com'}, 44 | defaultTitle: 'site name', 45 | link: [ 46 | {rel: 'link rel', href: 'link href'}, 47 | {rel: 'canonical', href: '/canonical'}, 48 | {rel: 'shortcut icon', href: '/favicon.ico'}, 49 | { 50 | rel: 'icon', 51 | type: 'image/png', 52 | href: '/favicon-16.png', 53 | sizes: '16x16', 54 | }, 55 | { 56 | rel: 'apple-touch-icon-precomposed', 57 | href: '/favicon-144.png', 58 | sizes: '144x144', 59 | }, 60 | {rel: 'apple-touch-startup-image', href: '/image.jpg'}, 61 | ], 62 | meta: [ 63 | {name: 'meta name', content: 'meta content'}, 64 | {name: 'application-name', content: 'site name'}, 65 | {name: 'msapplication-TileImage', content: '/favicon-144.png'}, 66 | {name: 'msapplication-square70x70logo', content: '/favicon-70.png'}, 67 | {name: 'description', content: 'description text'}, 68 | {property: 'og:k1', content: 'v1'}, 69 | {property: 'og:site_name', content: 'site name'}, 70 | {property: 'og:title', content: 'page title'}, 71 | {property: 'og:description', content: 'description text'}, 72 | {property: 'og:image', content: '/image.jpg'}, 73 | {property: 'og:url', content: '/page.url'}, 74 | {property: 'twitter:k3', content: 'v3'}, 75 | {property: 'twitter:title', content: 'page title'}, 76 | {property: 'twitter:description', content: 'description text'}, 77 | {property: 'twitter:image', content: '/image.jpg'}, 78 | {property: 'twitter:url', content: '/page.url'}, 79 | {property: 'ns:k2', content: 'v2'}, 80 | ], 81 | title: 'page title', 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | --require ./test/setup.js 3 | --reporter spec 4 | --timeout 5000 5 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | chai.use(require('chai-as-promised')); 3 | --------------------------------------------------------------------------------