gatsby-theme-q3 3.1.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,32 +1,89 @@
1
1
  import React from 'react';
2
+ import { get, isFunction, isObject } from 'lodash';
2
3
  import PropTypes from 'prop-types';
3
4
  import { Helmet } from 'react-helmet';
5
+ import { browser } from 'q3-ui-helpers';
4
6
  import useSiteMetaData from './useSiteMetaData';
5
7
 
8
+ const withContent = (output) => (content) =>
9
+ content && isFunction(output) ? output(content) : [];
10
+
11
+ export const getStartUrl = () =>
12
+ browser.isBrowserReady()
13
+ ? get(window, 'location.host')
14
+ : '';
15
+
16
+ export const generateMetaDescriptionOptions = withContent(
17
+ (content) => [
18
+ {
19
+ name: 'description',
20
+ content,
21
+ },
22
+ {
23
+ property: 'og:description',
24
+ content,
25
+ },
26
+ {
27
+ name: 'twitter:description',
28
+ content,
29
+ },
30
+ ],
31
+ );
32
+
33
+ export const generateMetaTitleOptions = withContent(
34
+ (content) => [
35
+ {
36
+ property: 'og:title',
37
+ content,
38
+ },
39
+ {
40
+ name: 'twitter:title',
41
+ content,
42
+ },
43
+ ],
44
+ );
45
+
46
+ export const generateBrand = (xs) =>
47
+ xs ? `%s | ${xs}` : undefined;
48
+
49
+ export const generateIcons = (site = {}) =>
50
+ site?.favicon
51
+ ? [
52
+ {
53
+ src: site.favicon,
54
+ sizes: '512x512',
55
+ type: 'image/png',
56
+ },
57
+ ]
58
+ : [];
59
+
60
+ export const generateManifest = (site = {}) => ({
61
+ background_color: site.color,
62
+ description: site.description,
63
+ display: 'fullscreen',
64
+ icons: generateIcons(site),
65
+ name: site.title,
66
+ start_url: getStartUrl(),
67
+ short_name: site.brand,
68
+ theme_color: site.color,
69
+ });
70
+
6
71
  const SEO = ({ description, lang, meta, title }) => {
7
72
  const site = useSiteMetaData();
8
73
  const metaDescription = description || site.description;
74
+ const metaTitle = title || site.title;
75
+ const manifestData = generateManifest(site);
9
76
 
10
77
  return (
11
78
  <Helmet
12
79
  htmlAttributes={{
13
80
  lang,
14
81
  }}
15
- title={title || site.title}
16
- titleTemplate={`%s | ${site.brand}`}
82
+ title={metaTitle}
83
+ titleTemplate={generateBrand(site.brand)}
17
84
  meta={[
18
- {
19
- name: 'description',
20
- content: metaDescription,
21
- },
22
- {
23
- property: 'og:title',
24
- content: title,
25
- },
26
- {
27
- property: 'og:description',
28
- content: metaDescription,
29
- },
85
+ ...generateMetaTitleOptions(metaTitle),
86
+ ...generateMetaDescriptionOptions(metaDescription),
30
87
  {
31
88
  property: 'og:type',
32
89
  content: 'website',
@@ -35,16 +92,18 @@ const SEO = ({ description, lang, meta, title }) => {
35
92
  name: 'twitter:card',
36
93
  content: 'summary',
37
94
  },
38
- {
39
- name: 'twitter:title',
40
- content: title,
41
- },
42
- {
43
- name: 'twitter:description',
44
- content: metaDescription,
45
- },
46
95
  ].concat(meta)}
47
- />
96
+ >
97
+ {isObject(manifestData) ? (
98
+ <link
99
+ rel="manifest"
100
+ href={`data:application/manifest+json,${encodeURIComponent(
101
+ JSON.stringify(manifestData),
102
+ )}`}
103
+ />
104
+ ) : null}
105
+ <link rel="icon" href={site.favicon} />
106
+ </Helmet>
48
107
  );
49
108
  };
50
109
 
@@ -1,38 +1,14 @@
1
1
  /* eslint-disable import/no-extraneous-dependencies */
2
2
  import React from 'react';
3
- import axios from 'axios';
4
3
  import PropTypes from 'prop-types';
5
4
  import AuthProvider from 'q3-ui-permissions';
6
- import LocaleBundles from './LocaleBundles';
7
5
 
8
- const setBaseUrlForRest = (
9
- baseURL = process.env.GATSBY_APP_BASE_URL ||
10
- 'http://localhost:9000',
11
- ) => {
12
- axios.defaults.baseURL = baseURL;
13
- return axios.defaults;
14
- };
15
-
16
- const Wrapper = ({ baseURL, children, locale }) => {
17
- setBaseUrlForRest(baseURL);
18
-
19
- return (
20
- <LocaleBundles locale={locale}>
21
- <AuthProvider>{children}</AuthProvider>
22
- </LocaleBundles>
23
- );
24
- };
25
-
26
- Wrapper.defaultProps = {
27
- baseURL: undefined,
28
- };
6
+ const Wrapper = ({ children }) => (
7
+ <AuthProvider>{children}</AuthProvider>
8
+ );
29
9
 
30
10
  Wrapper.propTypes = {
31
- baseURL: PropTypes.string,
32
11
  children: PropTypes.node.isRequired,
33
-
34
- // eslint-disable-next-line
35
- locale: PropTypes.object.isRequired,
36
12
  };
37
13
 
38
14
  export default Wrapper;
@@ -0,0 +1,58 @@
1
+ import {
2
+ generateMetaDescriptionOptions,
3
+ getStartUrl,
4
+ generateIcons,
5
+ generateBrand,
6
+ } from '../SearchEngine';
7
+
8
+ jest.mock('q3-ui-locale', () => ({
9
+ browser: {
10
+ isBrowserReady: jest.fn(),
11
+ },
12
+ }));
13
+
14
+ const host = 'https://google.ca';
15
+
16
+ beforeEach(() => {
17
+ Object.defineProperty(window, 'location', {
18
+ value: {
19
+ host,
20
+ },
21
+ });
22
+ });
23
+
24
+ describe('SearchEngine', () => {
25
+ it('should not render descriptions without content', () => {
26
+ expect(generateMetaDescriptionOptions().length).toBe(0);
27
+ });
28
+
29
+ it('should render descriptions with content', () => {
30
+ expect(
31
+ generateMetaDescriptionOptions('foo').length,
32
+ ).toBeGreaterThanOrEqual(1);
33
+ });
34
+
35
+ it('should return host', () => {
36
+ expect(getStartUrl()).toMatch(host);
37
+ });
38
+
39
+ it('should render favicon', () => {
40
+ expect(
41
+ generateIcons({
42
+ favicon: host,
43
+ }),
44
+ ).toHaveLength(1);
45
+ });
46
+
47
+ it('should not render favicon', () => {
48
+ expect(
49
+ generateIcons({
50
+ favicon: undefined,
51
+ }),
52
+ ).toHaveLength(0);
53
+ });
54
+
55
+ it('should include template literals', () => {
56
+ expect(generateBrand('3merge')).toMatch('%s | 3merge');
57
+ });
58
+ });
@@ -1,22 +1,23 @@
1
- import { get } from 'lodash';
1
+ import { get, merge } from 'lodash';
2
2
  import { useStaticQuery, graphql } from 'gatsby';
3
+ import useRunTime from 'gatsby-theme-q3-mui/src/components/useRunTime';
3
4
 
4
5
  export default () =>
5
- get(
6
- useStaticQuery(graphql`
7
- query {
8
- site {
9
- siteMetadata {
10
- appDirectory
11
- brand
12
- description
13
- favicon
14
- logo
15
- title
6
+ merge(
7
+ get(
8
+ useStaticQuery(graphql`
9
+ query {
10
+ site {
11
+ siteMetadata {
12
+ appDirectory
13
+ description
14
+ title
15
+ }
16
16
  }
17
17
  }
18
- }
19
- `),
20
- 'site.siteMetadata',
21
- {},
18
+ `),
19
+ 'site.siteMetadata',
20
+ {},
21
+ ),
22
+ useRunTime(),
22
23
  );
@@ -1,73 +0,0 @@
1
- import { get } from 'lodash';
2
- import config from '../gatsby-config';
3
-
4
- const CANONICAL = 'gatsby-plugin-canonical-urls';
5
- const MANIFEST = 'gatsby-plugin-manifest';
6
- const ROBOTS = 'gatsby-plugin-robots-txt';
7
-
8
- const ENV = {
9
- contentfulSpaceID: 1,
10
- contentfulAccessToken: 1,
11
- };
12
-
13
- const containsResolver = (plugins = [], name) =>
14
- plugins.find(
15
- (p) => typeof p === 'object' && p.resolve === name,
16
- );
17
-
18
- const checkPlugins = (args = {}, plugin) => {
19
- const { plugins } = config({ ...ENV, ...args });
20
- const statement = expect(
21
- containsResolver(plugins, plugin),
22
- );
23
-
24
- return {
25
- has: () => statement.not.toBeUndefined(),
26
- hasNot: () => statement.toBeUndefined(),
27
- };
28
- };
29
-
30
- describe('gatsby-config', () => {
31
- describe('plugins', () => {
32
- it('should error without contentful access token', () => {
33
- process.env.URL =
34
- 'https://development.netlify.3merge.com';
35
- expect(() =>
36
- config({
37
- contentfulSpaceID: '1',
38
- }),
39
- ).toThrowError();
40
- });
41
-
42
- it('should include conditional plugins', () =>
43
- [CANONICAL, MANIFEST].forEach((name) =>
44
- checkPlugins(
45
- {
46
- brandingColor: '#FFF',
47
- title: 'Foo',
48
- siteUrl: 'https://google.ca',
49
- },
50
- name,
51
- ).has(),
52
- ));
53
-
54
- it('should exclude conditional plugins', () =>
55
- [CANONICAL, MANIFEST].forEach((name) =>
56
- checkPlugins({}, name).hasNot(),
57
- ));
58
-
59
- it.each([
60
- ['https://dev.netlify.3merge.com', 'disallow'],
61
- ['https://3merge.com', 'allow'],
62
- ])('should disable indexing', (url, key) => {
63
- process.env.URL = url;
64
- const { plugins } = config({ ...ENV });
65
- const res = containsResolver(plugins, ROBOTS);
66
- const prod = get(
67
- res,
68
- 'options.env.production.policy',
69
- )[0];
70
- expect(prod).toHaveProperty(key, '/');
71
- });
72
- });
73
- });
@@ -1,13 +0,0 @@
1
- const path = require('path');
2
- const loadContent = require('../loadContent');
3
-
4
- describe('loadContent', () => {
5
- it('should fetch all content from directory', () => {
6
- const out = loadContent(
7
- path.resolve(__dirname, '../../__fixtures__'),
8
- );
9
-
10
- expect(out).toHaveProperty('en');
11
- expect(out).toHaveProperty('fr');
12
- });
13
- });
@@ -1,139 +0,0 @@
1
- const {
2
- genCursor,
3
- appendSiblingsToContext,
4
- getPreviousArchiveUrl,
5
- getNextArchiveUrl,
6
- getNumberOfPages,
7
- paginateArchiveContext,
8
- } = require('../pagination');
9
-
10
- const genEntries = () => {
11
- const entries = [];
12
- for (let i = 0; i < 30; i += 1) entries.push(i);
13
- return entries;
14
- };
15
-
16
- describe('pagination', () => {
17
- describe('"genCursor"', () => {
18
- const stub = ['foo', 'bar', 'quuz', 'garply'];
19
- const cursor = genCursor(stub, 2);
20
- // current index targets "quuz"
21
-
22
- it('should identify first item', () => {
23
- expect(cursor.first).toMatch('foo');
24
- });
25
-
26
- it('should identify last item', () => {
27
- expect(cursor.last).toMatch('garply');
28
- });
29
-
30
- it('should identify next item', () => {
31
- expect(cursor.next).toMatch('garply');
32
- });
33
-
34
- it('should identify previous item', () => {
35
- expect(cursor.prev).toMatch('bar');
36
- });
37
-
38
- it('should identify first position', () => {
39
- expect(cursor.isFirst).toBeFalsy();
40
- expect(genCursor(stub, 0).isFirst).toBeTruthy();
41
- });
42
-
43
- it('should identify last position', () => {
44
- expect(cursor.isLast).toBeFalsy();
45
- expect(genCursor(stub, 3).isLast).toBeTruthy();
46
- });
47
- });
48
-
49
- describe('"appendSiblingsToContext"', () => {
50
- const mockContentfulEntry = (id) => ({
51
- contentful_id: id,
52
- });
53
-
54
- const stubWithContentful = [
55
- mockContentfulEntry(1),
56
- mockContentfulEntry(2),
57
- mockContentfulEntry(3),
58
- ];
59
-
60
- it('should map contentful entries using cursor', () => {
61
- const entries = appendSiblingsToContext(
62
- stubWithContentful,
63
- );
64
- expect(entries[0]).toMatchObject({
65
- prev: 3,
66
- next: 2,
67
- });
68
- expect(entries[2]).toMatchObject({
69
- prev: 2,
70
- next: 1,
71
- });
72
- });
73
- });
74
-
75
- describe('"getPreviousArchiveUrl"', () => {
76
- it('should return null', () => {
77
- expect(getPreviousArchiveUrl('/foo', 1)).toBeNull();
78
- });
79
-
80
- it('should return archive', () => {
81
- expect(getPreviousArchiveUrl('/foo', 2)).toEqual(
82
- '/foo',
83
- );
84
- });
85
-
86
- it('should return archive sub-directory', () => {
87
- expect(getPreviousArchiveUrl('/foo', 3)).toEqual(
88
- '/foo/2',
89
- );
90
- });
91
- });
92
-
93
- describe('"getNextArchiveUrl"', () => {
94
- it('should return sub-directory', () => {
95
- expect(getNextArchiveUrl('/foo', 8, 9)).toEqual(
96
- '/foo/9',
97
- );
98
- });
99
-
100
- it('should return null', () => {
101
- expect(getNextArchiveUrl('/foo', 9, 9)).toBeNull();
102
- });
103
- });
104
-
105
- describe('"getNumberOfPages"', () => {
106
- it('should return number divisible by', () => {
107
- expect(getNumberOfPages(genEntries(), 5)).toBe(6);
108
- });
109
- });
110
-
111
- describe('"paginateArchiveContext"', () => {
112
- it('should return pagination meta', () => {
113
- // default 15 per page
114
- const res = paginateArchiveContext(
115
- genEntries(),
116
- '/foo',
117
- );
118
-
119
- expect(res).toHaveLength(2);
120
- expect(res[0]).toMatchObject({
121
- path: '/foo',
122
- limit: 15,
123
- skip: 0,
124
- pageNum: 0,
125
- prev: null,
126
- next: '/foo/2',
127
- });
128
-
129
- expect(res[1]).toMatchObject({
130
- path: '/foo/2',
131
- limit: 15,
132
- skip: 15,
133
- pageNum: 1,
134
- prev: '/foo',
135
- next: null,
136
- });
137
- });
138
- });
139
- });
@@ -1,21 +0,0 @@
1
- const slug = require('../slug');
2
-
3
- describe('slug', () => {
4
- it('should combine use slug attribute', () => {
5
- expect(
6
- slug({ slug: 'already-formatted-as-slug' }),
7
- ).toMatch('/already-formatted-as-slug');
8
- });
9
-
10
- it('should combine use title attribute', () => {
11
- expect(
12
- slug({ title: 'This is a post' }, 'foos'),
13
- ).toMatch('/foos/this-is-a-post');
14
- });
15
-
16
- it('should combine use name attribute', () => {
17
- expect(slug({ name: "Post's name" }, '/foos')).toMatch(
18
- '/foos/posts-name',
19
- );
20
- });
21
- });
@@ -1,42 +0,0 @@
1
- const { get } = require('lodash');
2
- const { resolve } = require('path');
3
- const {
4
- appendSiblingsToContext,
5
- paginateArchiveContext,
6
- } = require('./pagination');
7
-
8
- module.exports = ({
9
- archiveComponentRelativePath,
10
- createPage,
11
- detailComponentRelativePath,
12
- nodesKeyName,
13
- slug,
14
- }) => async ({ data, errors }) => {
15
- if (errors) throw errors;
16
- const { nodes = [] } = get(data, nodesKeyName, {
17
- nodes: [],
18
- });
19
-
20
- const archives = appendSiblingsToContext(nodes).map(
21
- (context) =>
22
- createPage({
23
- path:
24
- // see slugType for more details on this field
25
- context.to || `/${slug}/${context.contentful_id}`,
26
- component: resolve(detailComponentRelativePath),
27
- context,
28
- }),
29
- );
30
-
31
- const entries = paginateArchiveContext(nodes, slug).map(
32
- ({ path, ...context }) =>
33
- createPage({
34
- component: resolve(archiveComponentRelativePath),
35
- path,
36
- context,
37
- }),
38
- );
39
-
40
- await Promise.all(archives);
41
- await Promise.all(entries);
42
- };
package/helpers/index.js DELETED
@@ -1,19 +0,0 @@
1
- const ArchiveBuilder = require('./archive');
2
- const loadContent = require('./loadContent');
3
- const {
4
- appendSiblingsToContext,
5
- paginateArchiveContext,
6
- } = require('./pagination');
7
- const slug = require('./slug');
8
- const slugType = require('./slugType');
9
- const setup = require('./setup');
10
-
11
- module.exports = {
12
- ArchiveBuilder,
13
- loadContent,
14
- appendSiblingsToContext,
15
- paginateArchiveContext,
16
- setup,
17
- slug,
18
- slugType,
19
- };
@@ -1,45 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- const readJsonFile = (dir, filename) => {
5
- try {
6
- const file = path.resolve(dir, filename);
7
- const buffer = fs.readFileSync(file);
8
- return JSON.parse(buffer);
9
- } catch (e) {
10
- return {};
11
- }
12
- };
13
-
14
- const reduceFileSystem = (name, next) =>
15
- fs
16
- .readdirSync(name, { withFileTypes: true })
17
- .reduce(next, {});
18
-
19
- const getJsonFileNameFromDirent = ({ name }) =>
20
- path.basename(name, '.json');
21
-
22
- const readFilePathFromDirent = ({ name }, root, next) =>
23
- next(path.join(root, name));
24
-
25
- const recurseFileSystem = (pathName) =>
26
- reduceFileSystem(pathName, (curr, dirent) =>
27
- Object.assign(curr, {
28
- [getJsonFileNameFromDirent(
29
- dirent,
30
- )]: dirent.isDirectory()
31
- ? readFilePathFromDirent(
32
- dirent,
33
- pathName,
34
- recurseFileSystem,
35
- )
36
- : readJsonFile(pathName, dirent.name),
37
- }),
38
- );
39
-
40
- recurseFileSystem.readJsonFile = readJsonFile;
41
- recurseFileSystem.reduceFileSystem = reduceFileSystem;
42
- recurseFileSystem.getJsonFileNameFromDirent = getJsonFileNameFromDirent;
43
- recurseFileSystem.readFilePathFromDirent = readFilePathFromDirent;
44
-
45
- module.exports = recurseFileSystem;
@@ -1,10 +0,0 @@
1
- module.exports = (src) => {
2
- try {
3
- if (!src) throw new Error('No theme file detected');
4
-
5
- // eslint-disable-next-line
6
- return require(src);
7
- } catch (e) {
8
- return {};
9
- }
10
- };