├── .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 | [](https://travis-ci.org/joonhocho/react-hat)
3 | [](https://coveralls.io/github/joonhocho/react-hat?branch=master)
4 | [](https://badge.fury.io/js/react-hat)
5 | [](https://david-dm.org/joonhocho/react-hat)
6 | [](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 |
--------------------------------------------------------------------------------