gatsby-theme-q3 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- };