├── docs
├── example.png
├── example-objects.png
├── example-localised.png
├── localisation-settings.png
└── article.md
├── now.json
├── src
├── components
│ ├── layouts
│ │ ├── common
│ │ │ ├── Container.js
│ │ │ ├── GlobalStyles.js
│ │ │ └── Head.js
│ │ ├── Default.js
│ │ └── Blog.js
│ ├── molecules
│ │ ├── PostList.js
│ │ ├── Footer.js
│ │ └── Header.js
│ ├── modules
│ │ ├── SitePage.js
│ │ ├── BlogPost.js
│ │ ├── SitePostListing.js
│ │ └── withLocale.js
│ └── atoms
│ │ ├── HTMLContentArea.js
│ │ ├── PostTile.js
│ │ ├── HeaderNav.js
│ │ └── LocaleSelector.js
└── pages
│ └── index.js
├── gatsby-config.js
├── config.js
├── package.json
├── LICENSE
├── .gitignore
├── gatsby-node.js
└── README.md
/docs/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/gatsby-localization-app-starter/master/docs/example.png
--------------------------------------------------------------------------------
/docs/example-objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/gatsby-localization-app-starter/master/docs/example-objects.png
--------------------------------------------------------------------------------
/docs/example-localised.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/gatsby-localization-app-starter/master/docs/example-localised.png
--------------------------------------------------------------------------------
/docs/localisation-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/gatsby-localization-app-starter/master/docs/localisation-settings.png
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "gatsby-localisation-starter",
4 | "builds": [
5 | {
6 | "src": "package.json",
7 | "use": "@now/static-build",
8 | "config": { "distDir": "public" }
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/src/components/layouts/common/Container.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | import appConfig from '../../../../config';
4 |
5 | export default styled.div`
6 | max-width: ${
7 | ({ maxWidth = appConfig.responsive.defaultContainerMaxWidth }) => maxWidth};
8 | min-height: ${({ minHeight }) => minHeight};
9 | margin: 0 auto;
10 | padding: 0 1rem;
11 | `;
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | 'gatsby-plugin-emotion',
4 | 'gatsby-plugin-react-helmet',
5 | {
6 | resolve: `gatsby-source-cosmicjs`,
7 | options: {
8 | bucketSlug: 'minimal-gatsby-localisation-site',
9 | objectTypes: ['blog-posts', 'pages'],
10 | apiAccess: {
11 | read_key: ``,
12 | }
13 | }
14 | }
15 | ],
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/molecules/PostList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from '@emotion/styled';
3 |
4 | import PostTile from '../atoms/PostTile';
5 |
6 | const PostListContainer = styled.section`
7 | display: flex;
8 | `;
9 |
10 | export default function PostList({ posts, locale, ...rest }) {
11 | return (
12 |
13 | {posts.map(({ node: postNode }) => (
14 |
19 | ))}
20 |
21 | );
22 | }
--------------------------------------------------------------------------------
/src/components/layouts/common/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Global, css } from '@emotion/core';
3 | import { normalize } from 'polished';
4 |
5 | const normalizeStyles = normalize();
6 |
7 | const globalStyles = css`
8 | body {
9 | font-family: 'Open Sans', 'Franklin Gothic Medium', Arial, sans-serif;
10 | }
11 | `;
12 |
13 | export default () => (
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { graphql } from 'gatsby';
3 | import { Redirect } from '@reach/router';
4 |
5 | import withLocale from '../components/modules/withLocale';
6 |
7 | // redirect the index page based on the preferred locale
8 | // otherwise redirect to the first detected available locale
9 | export default withLocale(({ locale }) => (
10 |
14 | ), { fromLocation: false });
15 |
16 | // specifically query index site pages
17 | export const query = graphql`
18 | {
19 | allCosmicjsPages(filter: {slug: {eq: "index" }}){
20 | edges{
21 | node{
22 | slug
23 | locale
24 | }
25 | }
26 | }
27 | }
28 | `;
29 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | // shared config values used within various parts of the application
2 |
3 | module.exports = {
4 | // the default locale to prefer if no window.navigator values
5 | // can be resolved
6 | fallbackLocale: 'en-US',
7 | // fallback site title if none is provided via props
8 | fallbackSiteTitle: 'Gatsby Localization Website',
9 | // settings for default colors
10 | theme: {
11 | primaryAccent: '#8ad',
12 | // speed to do standard ui transitions
13 | primaryTransitionSpeed: '.2s',
14 | },
15 | // settings for responsive/layout configuration
16 | responsive: {
17 | // max width size for a default static page
18 | defaultContainerMaxWidth: '960px',
19 | // default minimum page height
20 | defaultMinimumPageHeight: 'calc(100vh - 256px)',
21 | // max width size for content blog posts
22 | blogPostContainerMaxWidth: '760px',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/layouts/common/Head.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Helmet } from 'react-helmet';
3 |
4 | import appConfig from '../../../../config';
5 |
6 | function generatePageTitle(title, siteTitle) {
7 | const finalTitleParts = [];
8 |
9 | if (title) {
10 | // prepend page-specific title if provided
11 | finalTitleParts.push(title);
12 | }
13 |
14 | finalTitleParts.push(siteTitle || appConfig.fallbackSiteTitle);
15 |
16 | // trim surrounding whitespace of each part
17 | // separate with pipe character if more than one part provided
18 | return finalTitleParts
19 | .map(part => part.trim())
20 | .join(' | ');
21 | }
22 |
23 | export default function Head({
24 | title, siteTitle, children,
25 | }) {
26 | return (
27 |
28 | {generatePageTitle(title, siteTitle)}
29 | {children}
30 |
31 | );
32 | }
--------------------------------------------------------------------------------
/src/components/molecules/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from '@emotion/styled';
3 | import { readableColor, lighten } from 'polished';
4 |
5 | import appConfig from '../../../config';
6 |
7 | import Container from '../layouts/common/Container';
8 |
9 | import LocaleSelector from '../atoms/LocaleSelector';
10 |
11 | const { primaryAccent } = appConfig.theme;
12 |
13 | export const FooterWrapper = styled.footer`
14 | padding: 2.5rem 1rem;
15 | background-color: ${lighten(0.2, primaryAccent)};
16 | border-top: solid 5px ${lighten(0.15, primaryAccent)};
17 | color: ${readableColor(primaryAccent)};
18 | `;
19 |
20 | export default function Footer({ locale, children }) {
21 | return (
22 |
23 |
24 |
27 | {children}
28 |
29 |
30 | );
31 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-localisation-starter",
3 | "description": "Gatsby example site using gatsby-source-cosmicjs",
4 | "license": "MIT",
5 | "scripts": {
6 | "develop": "gatsby develop",
7 | "build": "gatsby build",
8 | "now-build": "npm run build",
9 | "start": "npm run develop",
10 | "serve": "gatsby serve"
11 | },
12 | "dependencies": {
13 | "@emotion/core": "^10.0.10",
14 | "@emotion/styled": "^10.0.10",
15 | "gatsby": "^2.0.17",
16 | "gatsby-plugin-emotion": "^4.0.6",
17 | "gatsby-plugin-react-helmet": "^3.0.12",
18 | "gatsby-source-cosmicjs": "0.0.7",
19 | "htmr": "^0.7.0",
20 | "moment": "^2.24.0",
21 | "polished": "^3.2.0",
22 | "react": "^16.3.2",
23 | "react-dom": "^16.3.2",
24 | "react-helmet": "^5.2.0"
25 | },
26 | "devDependencies": {
27 | "eslint": "^4.19.1",
28 | "eslint-plugin-react": "^7.11.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/modules/SitePage.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { graphql } from 'gatsby';
3 |
4 | import DefaultLayout from '../layouts/Default';
5 | import HTMLContentArea from '../atoms/HTMLContentArea';
6 | import withLocale from './withLocale';
7 |
8 | function SitePage({ data, locale }) {
9 | const {
10 | cosmicjsPages: page,
11 | allCosmicjsPages: { edges: allPages },
12 | } = data;
13 |
14 | return (
15 |
19 | {page.content}
20 |
21 | }
22 | locale={locale}
23 | allPages={allPages}
24 | />
25 | );
26 | }
27 |
28 | export default withLocale(SitePage);
29 |
30 | export const query = graphql`
31 | query SitePageQuery($id: String!) {
32 | cosmicjsPages(id: { eq: $id }) {
33 | title
34 | content
35 | }
36 | allCosmicjsPages {
37 | edges{
38 | node{
39 | locale
40 | title
41 | slug
42 | }
43 | }
44 | }
45 | }
46 | `;
--------------------------------------------------------------------------------
/src/components/layouts/Default.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import appConfig from '../../../config';
4 |
5 | import GlobalStyles from './common/GlobalStyles';
6 | import Container from './common/Container';
7 | import Head from './common/Head';
8 |
9 | import Header from '../molecules/Header';
10 | import Footer from '../molecules/Footer';
11 |
12 | export default function DefaultLayout({
13 | title, siteTitle, headerBackgroundColor,
14 | content, locale, allPages,
15 | }) {
16 | return (
17 |
18 |
19 |
23 |
30 |
33 | {content}
34 |
35 |
38 |
39 | );
40 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019
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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # Bower dependency directory (https://bower.io/)
24 | bower_components
25 |
26 | # node-waf configuration
27 | .lock-wscript
28 |
29 | # Compiled binary addons (http://nodejs.org/api/addons.html)
30 | build/Release
31 |
32 | # Dependency directories
33 | node_modules/
34 | jspm_packages/
35 |
36 | # Typescript v1 declaration files
37 | typings/
38 |
39 | # Optional npm cache directory
40 | .npm
41 |
42 | # Optional eslint cache
43 | .eslintcache
44 |
45 | # Optional REPL history
46 | .node_repl_history
47 |
48 | # Output of 'npm pack'
49 | *.tgz
50 |
51 | # Yarn Integrity file
52 | .yarn-integrity
53 |
54 | # dotenv environment variables file
55 | .env
56 |
57 | .cache/
58 | public
59 | yarn-error.log
60 |
--------------------------------------------------------------------------------
/src/components/layouts/Blog.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import appConfig from '../../../config';
4 |
5 | import GlobalStyles from './common/GlobalStyles';
6 | import Head from './common/Head';
7 | import Container from './common/Container';
8 |
9 | import Header from '../molecules/Header';
10 | import Footer from '../molecules/Footer';
11 |
12 | export default function BlogLayout({
13 | title, siteTitle, headerBackgroundColor,
14 | contentHeading, content, locale, allPages,
15 | }) {
16 | return (
17 |
18 |
19 |
23 |
31 |
32 | {contentHeading}
33 |
34 |
35 |
39 | {content}
40 |
41 |
44 |
45 | );
46 | }
--------------------------------------------------------------------------------
/src/components/atoms/HTMLContentArea.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from '@emotion/styled';
3 | import renderHTML from 'htmr';
4 |
5 | // default styles for static html fetched from cosmic js
6 | import { readableColor, darken } from 'polished';
7 |
8 | import appConfig from '../../../config';
9 |
10 | const { primaryAccent } = appConfig.theme;
11 |
12 | // This is where HTML content stylings would go
13 | const HTMLContentAreaWrapper = styled.div`
14 | margin: 2rem 0 2rem;
15 | line-height: 1.3;
16 |
17 | h1 { font-size: 1.7rem }
18 | h2 { font-size: 1.5rem }
19 | h3 { font-size: 1.2rem }
20 | h4 { font-size: 1.15rem }
21 | h5 { font-size: 1.06rem }
22 |
23 | h1, h2, h3, h4, h5 {
24 | color: ${darken(0.2, primaryAccent)};
25 | }
26 |
27 | b, strong {
28 | font-weight: 600;
29 | }
30 |
31 | i, em {
32 | font-style: italic;
33 | }
34 |
35 | blockquote {
36 | opacity: 0.6;
37 | }
38 |
39 | img {
40 | display: block;
41 | margin: 0 auto;
42 | }
43 | `;
44 |
45 | export default function HTMLContentArea({ children, ...rest }) {
46 | return (
47 |
48 | {renderHTML(children)}
49 |
50 | );
51 | }
--------------------------------------------------------------------------------
/src/components/modules/BlogPost.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { graphql } from 'gatsby';
3 |
4 | import BlogLayout from '../layouts/Blog';
5 | import HTMLContentArea from '../atoms/HTMLContentArea';
6 | import withLocale from './withLocale';
7 |
8 | function BlogPost({ data, locale }) {
9 | const {
10 | cosmicjsBlogPosts: post,
11 | allCosmicjsPages: { edges: allPages },
12 | } = data;
13 |
14 | return (
15 |
19 | {post.content}
20 |
21 | }
22 | locale={locale}
23 | allPages={allPages}
24 | />
25 | );
26 | }
27 |
28 | export default withLocale(BlogPost, { resolveFrom: 'allCosmicjsBlogPosts' });
29 |
30 | export const query = graphql`
31 | query BlogPostQuery($id: String!, $slug: String!) {
32 | cosmicjsBlogPosts(id: { eq: $id }) {
33 | title
34 | content
35 | modified_at
36 | created_at
37 | }
38 | allCosmicjsBlogPosts(filter: {slug: {eq: $slug}}){
39 | edges{
40 | node{
41 | locale
42 | slug
43 | }
44 | }
45 | }
46 | allCosmicjsPages {
47 | edges{
48 | node{
49 | locale
50 | title
51 | slug
52 | }
53 | }
54 | }
55 | }
56 | `;
--------------------------------------------------------------------------------
/src/components/atoms/PostTile.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from '@emotion/styled';
3 | import { Link } from 'gatsby';
4 | import moment from 'moment';
5 | import { readableColor, darken } from 'polished';
6 |
7 | import appConfig from '../../../config';
8 |
9 | const { primaryAccent } = appConfig.theme;
10 |
11 | const PostTileWrapper = styled.article`
12 | position: relative;
13 | width: 300px;
14 | padding: 1rem;
15 |
16 | background-color: rgba(0,0,0,0.05);
17 |
18 | h2 {
19 | color: ${darken(0.2, primaryAccent)};
20 | margin-top: 0;
21 | line-height: 1;
22 | }
23 |
24 | margin: 0 1rem 1rem 0;
25 |
26 | @media(max-width: 600px) {
27 | margin: 0 0 1rem;
28 | }
29 | `;
30 |
31 | const PostTileLink = styled(Link)`
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | width: 100%;
36 | height: 100%;
37 | display: block;
38 | `;
39 |
40 | export default function PostTile({
41 | title, slug, created_at: createdAt, intendedLocale,
42 | }) {
43 | return (
44 |
45 |
46 |
47 | {title}
48 |
49 |
52 |
53 |
57 |
58 | );
59 | }
--------------------------------------------------------------------------------
/src/components/modules/SitePostListing.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from "react"
2 | import { graphql } from 'gatsby';
3 |
4 | import DefaultLayout from '../layouts/Default';
5 | import Container from '../layouts/common/Container';
6 | import HTMLContentArea from '../atoms/HTMLContentArea';
7 | import PostList from '../molecules/PostList';
8 | import withLocale from './withLocale';
9 |
10 | function BlogPostListing({ data, locale }) {
11 | const {
12 | cosmicjsPages: page,
13 | allCosmicjsBlogPosts: { edges: postsFromLocale },
14 | allCosmicjsPages: { edges: allPages },
15 | } = data;
16 |
17 | return (
18 |
22 |
23 | {page.content}
24 |
25 |
29 |
30 | }
31 | locale={locale}
32 | allPages={allPages}
33 | />
34 | );
35 | }
36 |
37 | export default withLocale(BlogPostListing);
38 |
39 | export const query = graphql`
40 | query BlogPostListingQuery($id: String!, $locale: String!) {
41 | cosmicjsPages(id: { eq: $id }) {
42 | title
43 | content
44 | }
45 | allCosmicjsPages {
46 | edges{
47 | node{
48 | locale
49 | title
50 | slug
51 | }
52 | }
53 | }
54 | allCosmicjsBlogPosts(filter: {locale: {eq: $locale}}){
55 | edges{
56 | node{
57 | title
58 | slug
59 | modified_at
60 | created_at
61 | }
62 | }
63 | }
64 | }
65 | `;
--------------------------------------------------------------------------------
/src/components/molecules/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from '@emotion/styled';
3 | import { readableColor, darken } from 'polished';
4 |
5 | import appConfig from '../../../config';
6 |
7 | import Container from '../layouts/common/Container';
8 | import HeaderNav from '../atoms/HeaderNav';
9 |
10 | const { primaryTransitionSpeed } = appConfig.theme;
11 |
12 | const withBgColor = styleAction => ({ headerBackgroundColor = appConfig.theme.primaryAccent }) =>
13 | styleAction(headerBackgroundColor);
14 |
15 | export const HeaderWrapper = styled.header`
16 | padding: 1rem;
17 | background-color: ${withBgColor(bg => bg)};
18 | color: ${withBgColor(bg => readableColor(bg))};
19 | border-bottom: solid .4rem ${withBgColor(bg => darken(0.15, bg))};
20 | transition: color ${primaryTransitionSpeed}, background ${primaryTransitionSpeed};
21 | `;
22 |
23 | const HeaderSiteTitle = styled.h1`
24 | color: inherit;
25 | text-align: center;
26 | font-size: 80px;
27 | margin: .5rem 0;
28 | line-height: 1;
29 | text-shadow: 3px 3px 0px ${withBgColor(bg => darken(0.15, bg))};
30 | `;
31 |
32 | export default function Header({
33 | children, headerBackgroundColor,
34 | allPages, locale, title,
35 | siteTitle = appConfig.defaultSiteTitle,
36 | }) {
37 | return (
38 |
41 |
42 |
43 | {title || siteTitle}
44 |
45 |
49 |
50 |
51 | );
52 | }
--------------------------------------------------------------------------------
/src/components/atoms/HeaderNav.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from '@emotion/styled';
3 | import { Link } from 'gatsby';
4 | import { readableColor, darken } from 'polished';
5 |
6 | import appConfig from '../../../config';
7 |
8 | const HeaderNavigationWrapper = styled.nav`
9 | text-align: center;
10 | `;
11 |
12 | const HeaderNavigationList = styled.ul`
13 | padding: 0;
14 | margin: 0;
15 | list-style: none;
16 | `;
17 |
18 | const HeaderNavigationLink = styled(Link)`
19 | font-size: 1.2rem;
20 | padding: .4rem .8rem;
21 | font-weight: 600;
22 |
23 | &, &:visited, &:disabled, &:hover {
24 | text-decoration: none;
25 | color: inherit;
26 | }
27 |
28 | text-shadow: none;
29 | transition: text-shadow ${appConfig.theme.primaryTransitionSpeed};
30 |
31 | &:hover {
32 | text-shadow: 2px 2px 0px ${darken(0.09, appConfig.theme.primaryAccent)};
33 | }
34 | `;
35 |
36 | export default function HeaderNavigation({ intendedLocale, allPages }) {
37 | // determine all listable pages based on the currently selected
38 | // locale
39 | const availableCosmicPagesByLocale = allPages
40 | .filter(({ node }) => node.locale === intendedLocale)
41 | .sort(({ node }) => node.slug === 'index' ? -1 : 1)
42 |
43 | return (
44 |
45 |
46 | {availableCosmicPagesByLocale.map(({ node: pageNode }) => (
47 |
50 | {pageNode.title}
51 |
52 | ))}
53 |
54 |
55 | );
56 | }
--------------------------------------------------------------------------------
/src/components/atoms/LocaleSelector.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styled from '@emotion/styled';
3 | import { Link } from 'gatsby';
4 |
5 | import { readableColor, lighten } from 'polished';
6 | import appConfig from '../../../config';
7 |
8 | const LocaleSelectionList = styled.ul`
9 | list-style: none;
10 | margin: 0;
11 | padding: 0;
12 |
13 | & > li {
14 | display: inline-block;
15 | }
16 | `;
17 |
18 | const buildLocaleSelection = Comp => styled(Comp)`
19 | display: inline-block;
20 | font-weight: ${({ isActive }) => isActive ? '600' : '400'};
21 | border: 0;
22 | padding: .6rem;
23 | background-color: ${lighten(0.005, appConfig.theme.primaryAccent)};
24 | text-decoration: none;
25 |
26 | margin-right: .5rem;
27 |
28 | &:hover, &:visited, &:disabled {
29 | text-decoration: none;
30 | color: inherit;
31 | }
32 | `;
33 |
34 | const LocaleSelectionButton = buildLocaleSelection('button');
35 | const LocaleSelectionLink = buildLocaleSelection(Link);
36 |
37 | // provide a means of selecting a different locale variant of the current page within view
38 | export default function LocaleSelector({
39 | alternateResourceUrls,
40 | intendedLocale,
41 | availableLocales,
42 | }) {
43 | return (
44 |
45 | {availableLocales.map((localeString) => {
46 | const isActive = localeString === intendedLocale;
47 |
48 | // only render button if current active version
49 | const LocaleActionable = isActive ?
50 | LocaleSelectionButton : LocaleSelectionLink;
51 |
52 | return (
53 |
54 |
59 | {localeString}
60 |
61 |
62 | )
63 | })}
64 |
65 | );
66 | }
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const basicSitePagePath = path.resolve('./src/components/modules/SitePage.js');
4 | const postListingPagePath = path.resolve('./src/components/modules/SitePostListing.js');
5 |
6 | exports.createPages = async ({ actions, graphql }) => {
7 | // iteration helper for simplifying the mapping of Cosmic JS Nodes
8 | // to a locale-based path
9 | const pageFromCosmicNode = ({ component, relativeRoot = '' }) => ({ node: { id, locale, slug }}) => actions.createPage({
10 | // handle component as string building function, otherwise treat
11 | // as basic string
12 | component: typeof component === 'function' ? component(slug) : component,
13 | // index slugs will be treated as empty
14 | path: `/${locale}${relativeRoot}/${slug === 'index' ? '' : slug}`,
15 | // prefer `id` since it's actually unique (unlike shared slugs
16 | // between locales)
17 | context: { id, slug, locale },
18 | });
19 |
20 | const {
21 | allCosmicjsBlogPosts: { edges: blogPosts },
22 | allCosmicjsPages: { edges: sitePages },
23 | } = (await graphql(`
24 | {
25 | allCosmicjsBlogPosts{
26 | edges{
27 | node{
28 | id
29 | slug
30 | locale
31 | }
32 | }
33 | }
34 | allCosmicjsPages{
35 | edges{
36 | node{
37 | id
38 | slug
39 | locale
40 | }
41 | }
42 | }
43 | }
44 | `)).data;
45 |
46 | // iterate localised blog posts
47 | blogPosts.forEach(pageFromCosmicNode({
48 | component: path.resolve('./src/components/modules/BlogPost.js'),
49 | relativeRoot: '/posts',
50 | }));
51 |
52 | // iterate localised site pages
53 | sitePages.forEach(pageFromCosmicNode({
54 | component(slug) {
55 | // return post listing page specifically for the posts page
56 | if (slug === 'posts') return postListingPagePath;
57 | // otherwise treat as a generic page
58 | return basicSitePagePath;
59 | },
60 | }));
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gatsby Localization App Starter
2 | 
3 |
4 | ## [Demo](https://cosmicjs.com/apps/gatsby-localization-app-starter)
5 |
6 | ## Why
7 |
8 | 1. The ability to host your own localized site within Gatsby JS
9 | 2. Leverages the power of [Cosmic JS](https://cosmicjs.com) localization
10 |
11 | ## Installing the demo (from Cosmic JS App Section)
12 |
13 | Cosmic JS provides an easy-to-use means of creating your own clone of this starter application from their own web ui. If you don't like getting your hands too dirty, or just would like to give the starter app a test drive, you can [install the app here](https://cosmicjs.com/apps/gatsby-localization-app-starter). The prompts will guide you in creating an account and creating the necessary items.
14 |
15 | ## Installing the demo (from source)
16 |
17 | The project source can be found here on GitHub. After cloning the repository, you should have something that loosely resembles the following structure.
18 |
19 | ```
20 | .gitignore
21 | config.js
22 | gatsby-config.js
23 | gatsby-node.js
24 | package.json
25 | package-lock.json
26 | LICENSE
27 | src
28 | ├── components
29 | │ ├── atoms
30 | │ │ ├── HeaderNav.js
31 | │ │ ├── HTMLContentArea.js
32 | │ │ ├── LocaleSelector.js
33 | │ │ └── PostTile.js
34 | │ ├── layouts
35 | │ │ ├── Blog.js
36 | │ │ ├── common
37 | │ │ │ ├── Container.js
38 | │ │ │ ├── GlobalStyles.js
39 | │ │ │ └── Head.js
40 | │ │ └── Default.js
41 | │ ├── modules
42 | │ │ ├── BlogPost.js
43 | │ │ ├── SitePage.js
44 | │ │ ├── SitePostListing.js
45 | │ │ └── withLocale.js
46 | │ └── molecules
47 | │ ├── Footer.js
48 | │ ├── Header.js
49 | │ └── PostList.js
50 | └── pages
51 | └── index.js
52 | ```
53 |
54 | Also make sure you `npm install` so you have the needed repositories for running the Gatsby starter application.
55 |
56 | ### Configuring the base project
57 |
58 | The project itself has two main files that are intended to be configure. `gatsby-config.js` is responsible for specifying which Cosmic JS Bucket to source your content from, while the `config.js` file is used for configuring various front-end portions of your application.
59 |
60 | To ensure that Gatsby is able to read your Cosmic Js Bucket, you need to fill out the areas within `gatsby-config.js` that resemble the following:
61 |
62 |
63 | ```js
64 | module.exports = {
65 | plugins: [
66 | 'gatsby-plugin-emotion',
67 | 'gatsby-plugin-react-helmet',
68 | {
69 | resolve: `gatsby-source-cosmicjs`,
70 | options: {
71 | bucketSlug: 'minimal-gatsby-localisation-site',
72 | objectTypes: ['blog-posts', 'pages'],
73 | apiAccess: {
74 | read_key: ``,
75 | }
76 | }
77 | }
78 | ],
79 | }
80 | ```
81 |
82 | For the above, ensure that the `bucketSlug` values correlates with your own Cosmic JS Bucket, and that the `apiAccess.read_key` is present if you've configured your bucket to require it. Also make sure you have `blog post` and `pages` Nodes within your Bucket, otherwise the `gatsby-source-cosmicjs` will not have the correct Gatsby GraphQL queries (preventing you from running the demo)
83 |
84 | ## Running the demo
85 |
86 | To start the demo (in development mode) after all dependencies have been started, simply run `npm start`, or alternatively `npm run develop`. The project will then (by default) be accessible on port `8000` on `localhost`. The console output of the command will also let you know in case its been configured otherwise.
87 |
88 | The first thing you see should resemble.
89 |
90 | [Example]('./docs/example.png)
91 |
--------------------------------------------------------------------------------
/src/components/modules/withLocale.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import appConfig from '../../../config';
4 |
5 | // small helper utility for mapping an intended page
6 | // locale based on available locales/navigator values
7 |
8 | function determineLocalesFromProps({ data, pageContext: { slug } }, resolvefrom) {
9 | const allCosmicNodes = data[resolvefrom] && data[resolvefrom].edges;
10 | if (!Array.isArray(allCosmicNodes) || !allCosmicNodes.length) return [];
11 |
12 | const localisedCosmicNodes = slug ?
13 | // filter by provided resource slug
14 | allCosmicNodes
15 | .filter(({ node }) => node.slug === slug) :
16 | // otherwise assume the available nodes are
17 | // already sorted by slug
18 | allCosmicNodes;
19 |
20 | // determine detected resource locales based on presented Cosmic JS resource
21 | const detectedContentLocales = [];
22 |
23 | let index = localisedCosmicNodes.length;
24 | let currentNode;
25 |
26 | while (index--) {
27 | currentNode = localisedCosmicNodes[index].node;
28 | if (currentNode.locale) {
29 | detectedContentLocales.push(currentNode.locale)
30 | }
31 | }
32 |
33 | return detectedContentLocales;
34 | }
35 |
36 | function determinePreferredLocales(isClient) {
37 | const defaultLocales = [appConfig.fallbackLocale];
38 |
39 | if (isClient) {
40 | return window.navigator.languages || defaultLocales;
41 | }
42 |
43 | return defaultLocales;
44 | }
45 |
46 | function localeFromCurrentLocation(location, availableLocales) {
47 | let index = availableLocales.length;
48 |
49 | while (index--) {
50 | if (location.pathname.indexOf(`/${availableLocales[index]}`) === 0) {
51 | // the currently rendered resource is already mapped to a
52 | // present locale, so return it
53 | return availableLocales[index];
54 | }
55 | }
56 |
57 | // otherwise treat as though we did not find the correct locale
58 | return null;
59 | }
60 |
61 | const generateAlternateResourceUrls = (availableLocales, intendedLocale, location) =>
62 | Object.assign({}, ...availableLocales
63 | // don't include pairing for currently selected locale
64 | .filter(locale => locale !== intendedLocale)
65 | // map key-value pairing for locale -> alternativeUrl
66 | .map(alternativeLocale => ({
67 | [alternativeLocale]: location.pathname.replace(intendedLocale, alternativeLocale),
68 | }))
69 | );
70 |
71 | const withLocale = (WrappedComponent, options = {}) => props => {
72 | const {
73 | // the GraphQL data node to use to resolve locales from
74 | resolveFrom = 'allCosmicjsPages',
75 | // determine if we should map the intended locale from the
76 | // loaded resource path
77 | fromLocation = false,
78 | } = options;
79 |
80 | const isClient = typeof window === 'object' && window.navigator;
81 |
82 | // attempt to determine navigator-based locale preference
83 | const preferredLocales = determinePreferredLocales(isClient);
84 |
85 | // resolve available locales for the rendered Cosmic JS resource
86 | const availableLocales = determineLocalesFromProps(props, resolveFrom) || [];
87 |
88 | // determine most appropriate locale to use based on
89 | // availability, or default to the first available
90 | // also prefer current items locale if present
91 | const intendedLocale =
92 | // attempt to resolve a valid locale from the currently presented resource
93 | // only if not otherwise disabled
94 | (fromLocation && localeFromCurrentLocation(props.location, availableLocales)) ||
95 | // attempt to resolve a valid locate from the current resources pageContext
96 | (props.pageContext && props.pageContext.locale) ||
97 | // attempt to resolve a locale based on the the user preference
98 | availableLocales.find(locale => preferredLocales.includes(locale)) ||
99 | // otherwise fallback onto the first known available locale
100 | availableLocales[0];
101 |
102 | console.log({ props, preferredLocales, availableLocales })
103 |
104 | // generate key-value pairing of alternative paths for the given resource
105 | const alternateResourceUrls = generateAlternateResourceUrls(availableLocales, intendedLocale, props.location);
106 |
107 | return (
108 |
117 | )
118 | }
119 |
120 | export default withLocale;
--------------------------------------------------------------------------------
/docs/article.md:
--------------------------------------------------------------------------------
1 | ## Preface
2 |
3 | It's been quite a while since I last created a demo application for Cosmic JS, and it's been refreshing to see how much the platform has grown over the last year as it's taken on new endeavors and has solved new business challenges in the process. One of the improvements that's been added overtime are the substantial amount of new plugins and integrations, along with some nice hidden improvements to how Cosmic JS handles and manages localised content.
4 |
5 | ## Before We Start
6 |
7 | Cosmic JS is a CMS platform that lets you leverage your content in a way that's agnostic to any specific platform. Essentially, it decouples you from having to lock your content down to a single framework, enabling you to implement and extend your own content as you wish. One of the most recent additions to the Cosmic JS toolbelt is the [`gatsby-source-cosmicjs`](https://github.com/cosmicjs/gatsby-source-cosmicjs) plugin for Gatsby JS, allowing you to embed your Cosmic JS content into a statically generated React application.
8 |
9 | I'll be honest, I'm rather old-fashioned when it comes to storing static content. As someone with a background of storing content within JSON files and rendering with Pug/Jade, it's always refreshing to see how far content management strategies have come over the last decade. Gatsby is basically the effective way to leverage static hosting (for that sweet SEO gain), but still allowing you to leverage the abilities of a dynamic React environment.
10 |
11 | As a means of helping people like me who are into the essentials, and then building up from that, I've created a minimalistic Cosmic JS + Gatsby localisation starter application.
12 |
13 | ## What is localisation anyways?
14 |
15 | Localisation is the act of making your content accessible to individuals outside of the language boundary you are typically used to, or simply put, the act of making websites comprehensive for people who don't speak the same language. Website localisation is also a form of web accessibility, in the sense that by implementing it you are enabling a larger section of your preferred demographic to digest your content, which is great for everyone, including your add revenue, or just overall site traffic.
16 |
17 | ## Installing the demo (from Cosmic JS App Section)
18 |
19 | Cosmic JS provides an easy-to-use means of creating your own clone of this starter application from their own web ui. If you don't like getting your hands too dirty, or just would like to give the starter app a test drive, you can access it from here. The prompts will guide you in creating an account and creating the necessary items.
20 |
21 | ## Installing the demo (from source)
22 |
23 | The project source can be found [here on GitHub](https://github.com/flynnham/comsicjs-gatsby-localisation-starter). After cloning the repository, you should have something that loosely resembles the following structure.
24 |
25 | ```
26 | .gitignore
27 | config.js
28 | gatsby-config.js
29 | gatsby-node.js
30 | package.json
31 | package-lock.json
32 | LICENSE
33 | src
34 | ├── components
35 | │ ├── atoms
36 | │ │ ├── HeaderNav.js
37 | │ │ ├── HTMLContentArea.js
38 | │ │ ├── LocaleSelector.js
39 | │ │ └── PostTile.js
40 | │ ├── layouts
41 | │ │ ├── Blog.js
42 | │ │ ├── common
43 | │ │ │ ├── Container.js
44 | │ │ │ ├── GlobalStyles.js
45 | │ │ │ └── Head.js
46 | │ │ └── Default.js
47 | │ ├── modules
48 | │ │ ├── BlogPost.js
49 | │ │ ├── SitePage.js
50 | │ │ ├── SitePostListing.js
51 | │ │ └── withLocale.js
52 | │ └── molecules
53 | │ ├── Footer.js
54 | │ ├── Header.js
55 | │ └── PostList.js
56 | └── pages
57 | └── index.js
58 | ```
59 |
60 | Also make sure you `npm install` so you have the needed repositories for running the Gatsby starter application.
61 |
62 | ### Configuring the base project
63 |
64 | The project itself has two main files that are intended to be configure. `gatsby-config.js` is responsible for specifying which Cosmic JS Bucket to source your content from, while the `config.js` file is used for configuring various front-end portions of your application.
65 |
66 | To ensure that Gatsby is able to read your Cosmic Js Bucket, you need to fill out the areas within `gatsby-config.js` that resemble the following:
67 |
68 |
69 | ```js
70 | module.exports = {
71 | plugins: [
72 | 'gatsby-plugin-emotion',
73 | 'gatsby-plugin-react-helmet',
74 | {
75 | resolve: `gatsby-source-cosmicjs`,
76 | options: {
77 | bucketSlug: 'minimal-gatsby-localisation-site',
78 | objectTypes: ['blog-posts', 'pages'],
79 | apiAccess: {
80 | read_key: ``,
81 | }
82 | }
83 | }
84 | ],
85 | }
86 | ```
87 |
88 | For the above, ensure that the `bucketSlug` values correlates with your own Cosmic JS Bucket, and that the `apiAccess.read_key` is present if you've configured your ucket to require it. Also make sure you have `blog post` and `pages` Nodes within your Bucket, otherwise the `gatsby-source-cosmicjs` will not have the correct Gatsby GraphQL queries (preventing you from running the demo)
89 |
90 | ## Running the demo
91 |
92 | To start the demo (in development mode) after all dependencies have been started, simply run `npm start`, or alternatively `npm run develop`. The project will then (by default) be accessible on port `8000` on `localhost`. The console output of the command will also let you know in case its been configured otherwise.
93 |
94 | The first thing you see should resemble.
95 |
96 | 
97 |
98 |
99 | ## How it works
100 |
101 | This localisation starter app in particular relies heavily on the `locale` value that's bound to each Cosmic JS Object. While each Object would normally require each item to have a unique `slug` constraint (meaning that slug would need to be unique among all other nodes of a given type): because we'd have localisation enabled for this particular projection, we no longer need to worry about that. With localisation, a Object can have have the same `slug` value given that each variant also has a unique `locale` specified.
102 |
103 | The `withLocale` component from `src/components/modules/withLocale.js` contains login that's responsible that a connected Gatsby page, can resolve which locale a given Page or Blog Post should be shown with, and any alternative links based on a resources available locale variations. So for example, if a blog post with a slug of `example-post` has locale variants in `en-US` and `es-AR`, the correct page to show will be determined by:
104 |
105 | * The `locale` specified in the Gatsby `pageContext` value (if present)
106 | * The `locale` detected from the url
107 | * The first available `locale` value of the resource being presented
108 |
109 | Each of these values are compared against the value of `window.navigator.languages` and the value of `fallbackLocale` within `config.js`. This is helpful in situations where the content type being requested isn't actually available for a specific Page or Blog Post, or when the page has no contextually available `locale`, such in the case of `src/pages.index`, which will redirect to the most preferable locale based on browser config.
110 |
111 | `withLocale` is also responsible for resolving a value called `alternateResourceUrls`, which is a key-value mapping of alternate locales of the currently shown page. This is used by components such as the `src/components/atoms/LocaleSelector`.
112 |
113 |
114 | ## Project structure
115 |
116 | The overall design structure of this application is minimal, loosely following the Atomic Design principles, in the sense that:
117 |
118 | * `src/components/atoms` contains components that don't solely depend on other complex child components
119 | * `src/components/layouts` contains components responsible for grouping various components together
120 | * `src/components/modules` contains components responsible for representing connected (dynamic values)
121 | * `src/components/molecules` contains components that are abstract configurations of `atoms`
122 |
123 | `src/pages` is responsible for storing static Gatsby pages that are usually mapped 1-1 like normal html pages. As such, files named `index` are treated as directory files, and files named anything represent a static path relative to the root.
124 |
125 | ### Why does this starter project not have any static query pages?
126 |
127 | As a localised starter application, the content being rendered is inherently dynamic, since it's not capable of being mapped easily within a static application without requiring path subsets. Adding a static React page (i.e. `localhost:8000/pictures-of-cats`) would be the equivalent of making a `default export` React component in `src/pages/pictures-of-cats.js`.
128 |
129 | ### Where are the css files?
130 |
131 | This starter application leverages a CSS-in-JS technology called `emotionjs`, which removes the requirement of serving static css bound to `className` attributes, since the styles themselves are bound when they are mounted. In SSR this is beneficial since it's deterministic nature allows more predictable style hydration and preloading.
132 |
133 | #### Creating an styled component or usable css class
134 |
135 | Emotion provides _two*_ main ways of generating reusable styles for components.
136 |
137 | One is via `@motion/styled`, which enables you to generate elements with bound `className` attributes.
138 |
139 | ```js
140 | import React from 'react';
141 | import styled from '@emotion/styled';
142 |
143 | // direct format
144 | const yourCoolStyledComponent = styled.div`
145 | color: red;
146 | `;
147 |
148 | // component wrapping format
149 | const SomeActualComponent = ({ className }) =>
150 | ;
151 |
152 | const yourCoolAlternateStyledComponent = styled(SomeActualComponent)`
153 | color: blue;
154 | `;
155 | ```
156 |
157 | The second form is via the `{ css }` helper from `@emotion/core`. It allows you to generate deterministic (and reusable) classnames;
158 |
159 | ```js
160 | import React from 'react';
161 | import { css } from '@emotion/core';
162 |
163 | const customCssClassName = css`
164 | color: green;
165 | `;
166 |
167 | const CoolDiv = () => (
168 |
169 | );
170 | ```
171 |
172 | There are more advanced examples of how to do this from the emotionjs documentation [here].
173 |
174 | ## Managing content
175 |
176 | In order for this starter application to work, a few assertions must be made.
177 |
178 | 1. You should already have `Pages` and `Blog Posts` Objects configured for your bucket
179 | 2. Each Object needs to have localisation enabled
180 | 3. Each `Page` and `Blog Post` should have the same `locales` available and should have each
181 | locale variant present.
182 |
183 | So your environment should look something like this:
184 |
185 | 
186 | 
187 |
188 | ### Enabling localisation for Cosmic Object Types
189 |
190 | On left left hand sidebar under `Dashboard` after creating each respective Object Type you should have `Blog Posts` and `Pages` as small folder icons visible under the dashboard. From that same interface, you should also see a small `cog` near the top of the listing. CLicking that will bring up an interface to enable localisation.
191 |
192 | 
193 |
194 | Toggle `Localisation` to `On` and then, add the locales you'd like to support for your application. These should be same same for both
195 |
196 | ## Conclusion
197 |
198 | Cosmic JS is an incredibly powerful means of allowing your content to be made available to various locations across the web, and it's localisation toolset lets to leverage this in a way where many people can benefit and appreciate your business or personal content, regardless of international barriers. Looking forward to see where the community will be able to take Cosmic JS to in the future.
199 |
--------------------------------------------------------------------------------