I am using Gatsby.js to create a blog. One of the starter packages seems to leverage an alias, so one can fetch and reuse an image to serve as a favicon. The favicon (alias) is in the src/ folder and it's called favicon.png.
That file is being rendered in this component:
import React from 'react';
import favicon from './favicon.png';
let inlinedStyles = '';
if (process.env.NODE_ENV === 'production') {
try {
/* eslint import/no-webpack-loader-syntax: off */
inlinedStyles = require('!raw-loader!../public/styles.css');
} catch (e) {
/* eslint no-console: "off" */
console.log(e);
}
}
export default class HTML extends React.Component {
render() {
let css;
if (process.env.NODE_ENV === 'production') {
css = <style id="gatsby-inlined-css" dangerouslySetInnerHTML={{ __html: inlinedStyles }} />;
}
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{/* Mobile Meta */}
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{/* Styles'n'Scripts */}
<link href="https://fonts.googleapis.com/css?family=Scope+One" rel="stylesheet" />
{this.props.headComponents}
<link rel="shortcut icon" href={favicon} />
{css}
</head>
<body>
<div id="___gatsby" dangerouslySetInnerHTML={{ __html: this.props.body }} />
{this.props.postBodyComponents}
</body>
</html>
);
}
}
This is what is rendered:
Can someone suggest an alternative way to get it to work?
Thanks in advance!
To add favicon in gatsby you should use gatsby-plugin-favicon :
Install
npm install --save gatsby-plugin-favicon
Use
Add your favicon.png in src folder (or subfolder).
In your gatsby-config.js add the plugin :
plugins: [
{
resolve: `gatsby-plugin-favicon`,
options: {
logo: "./src/path/to/your/favicon",
injectHTML: true,
icons: {
android: true,
appleIcon: true,
appleStartup: true,
coast: false,
favicons: true,
firefox: true,
twitter: false,
yandex: false,
windows: false
}
}
}
]
The plugin recommends a size of 1500x1500px for the favicon.
Doc here : https://github.com/Creatiwity/gatsby-plugin-favicon
Try using a template literal:
link={[
{ rel: 'shortcut icon', type: 'image/png', href: `${favicon}` }
]}
This worked for me (I am using react-helmet though).
See the following tutorials for more help:
https://www.michaelmanges.com/blog/add-a-favicon-to-your-gatsby-website/
https://atrost.com/posts/add-favicon-gatsby/
Related
i am updating dependencies for a client and i updated nextJS from getInitialProps to getServerSideProps.
Therefore, i had to update next-i18next.
Before, here's what was used in getInitialProps for translations :
class Homepage extends React.Component {
static async getInitialProps ({ store }) {
store.dispatch(PlansCreators.plansGet())
return {
namespacesRequired: ['common', 'containers', 'home', 'form']
}
}
render () {
return (
<Layout>
<Head>
<title>{`${config.DEV ? 'DEV - ' : ''} ${t('SEO.BASE.TITLE')}`}</title>
<meta name='description' content={t('SEO.BASE.DESCRIPTION')} />
<meta property='og:title' content={`${t('SEO.BASE.TITLE')}`} />
<meta property='og:description' content={t('SEO.BASE.DESCRIPTION')} />
<meta property='og:image' content='' />
</Head>
<Home />
</Layout>
)
}
}
After my update :
function Homepage() {
return (
<Layout>
<Head>
<title>{`${config.DEV ? 'DEV - ' : ''} ${t('SEO.BASE.TITLE')}`}</title>
<meta name="description" content={t('SEO.BASE.DESCRIPTION')} />
<meta property="og:title" content={`${t('SEO.BASE.TITLE')}`} />
<meta property="og:description" content={t('SEO.BASE.DESCRIPTION')} />
<meta property="og:image" content="" />
</Head>
<Home />
</Layout>
)
}
export async function getInitialProps({ locale, store }) {
store.dispatch(PlansCreators.plansGet())
return {
props: {
...(await serverSideTranslations(locale, ['common']))
}
}
}
The translations seems to be working during the render, but only for like 0.5 seconds. Then, it come back to SEO.BASE.TITLE... So i thought it was the localePath in the next-i18next.config.js that was involved, but that's not the case.
Here's my next-i18next.config.js :
module.exports = {
i18n: {
locales: ['fr', 'en'],
defaultLocale: 'fr',
reloadOnPrerender: process.env.NODE_ENV === 'development',
debug: process.env.NODE_ENV === 'development',
localePath:
typeof window === 'undefined'
? require('path').resolve('./public/locales')
: '/locales'
}
}
I also have this error in my javascript console (browser side) :
react_devtools_backend.js:4012 i18next: hasLoadedNamespace: i18next was not initialized
and
i18next::translator: key "SEO.ACCOUNT.TITLE" for languages "fr" won't get resolved as namespace "common" was not yet loaded This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!
Do i miss something ? Do i have to build a backend even with next-i18next ? I just follow the documentation 3 times...
Here are my versions :
"i18next": "^22.0.6",
"react-i18next": "^12.0.0",
"next-i18next": "^13.0.3",
Here's how i wrap my App in _app.tsx
export default compose(
withRedux(bootstrapStore),
withReduxSaga,
withRouter
)(appWithTranslation(MyApp, nextI18NextConfig))
Thanks for help.
I have updated this to use react-helmet-async in place of react-helmet.
I have a React application that has implemented server side rendering. I am following the steps on this site: Documentation for react-helmet-async, to implement the react-helmet-async functionality to dynamically assign meta and title. I am using a provider and have added the Helmet component to the App.js and the learnerSetupContainer.js scripts. When I look at the source code for both these pages I can that the title and meta for the home page is properly updated but the learnerSetupContainer is not being updated and is showing the same title and meta as the home page,
<title data-rh="true" itemprop="name" lang="en">Welcome to our Homepage</title>
<meta data-rh="true" name="description" content="Teach Learn Game"/><meta data-rh="true" name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"/>
I see other stackoverflow questions that say, "It's not documented in the readme, but you have to add data-react-helmet="true" to your meta tags that you want to replace with react-helmet." I tried adding this string but there was no change. Maybe I am not adding correctly. Can anyone see what I am doing wrong?
Below is the code I am using:
template.js
export default ({ markup, helmet , css }) => {
return `<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-1234567-9">
</script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-123456789-9');
</script>
<script data-ad-client="ca-pub-123456789012345" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<meta charset="utf-8">
<meta name="msvalidate.01" content="1234567890ASDFGHJJ" />
<meta name="google-site-verification" content="1234567890ASDFGHJKL" />
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<style>
a{
text-decoration: none;
color: #bb4d00
}
</style>
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id="root">${markup}</div>
<style id="jss-server-side">${css}</style>
<script type="text/javascript" src="/dist/bundle.js"></script>
</body>
</html>`;
};
express.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
app.get("*", (req, res) => {
const sheets = new ServerStyleSheets();
const context = {};
const helmetContext = {};
const markup = ReactDOMServer.renderToString(
//const markup = ReactDOMServer.renderToStaticMarkup(
sheets.collect(
<StaticRouter location={req.url} context={context}>
<ThemeProvider theme={theme}>
<HelmetProvider context = {helmetContext}>
<MainRouter />
</HelmetProvider>
</ThemeProvider>
</StaticRouter>
)
);
const {helmet} = helmetContext;
console.log ("Express - markup = ", markup);
console.log ("Express - helmet = ", helmet);
console.log ("Express - helmetContext = ", helmetContext);
console.log ("Express - helmet.title.toString() = ", helmet.title.toString());
console.log ("Express - helmet.meta.toString() = ", helmet.meta.toString());
if (context.url) {
return res.redirect(303, context.url);
}
const css = sheets.toString();
res.status(200).send(
Template({
markup: markup,
helmet: helmet,
css: css,
})
);
});
App.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
return (
<>
<React.StrictMode>
<BrowserRouter>
<ThemeProvider theme={responsivetheme}>
<SpeechDataProvider>
<CriteriaProvider>
<RandomOptionsProvider>
<HelmetProvider>
<MainRouter />
</HelmetProvider>
</RandomOptionsProvider>
</CriteriaProvider>
</SpeechDataProvider>
</ThemeProvider>
</BrowserRouter>
</React.StrictMode>
</>
);
MenuRouter.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
return (
<div>
<Helmet
data-rh="true"
htmlAttributes={{lang: "en", amp: undefined}} // amp takes no value
//titleTemplate="%s | React App"
titleAttributes={{itemprop: "name", lang: "en"}}
title="Welcome to our Homepage"
meta={[
{name: "description", content: "Teach Learn Game"},
{name: "viewport", content: "minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"},
]}
/>
</div>
learnerSetupContainer.js (only code relevant to react-helmet)
import {Helmet, HelmetProvider} from "react-helmet-async";
return (
<>
<div>
<Helmet>
<title data-rh="true">Welcome to our Learner Setup Page </title>
<meta data-rh="true" name="description" content="Game Options: Vocabulary, Math, History and Geography" />
<meta data-rh="true" name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
</Helmet>
</div>
</>
I would like to embed this code on my Gatsby site, what would be the best practice? I'm still learning and any help would be much appreciated.
Thanks
<script
id="nc-booking-widget-script"
src="https://www.neatcal.com/app/services/booking/assets/js/integrate.js?v=21.04.26.5"
data-access-key="fcd3a2ae209d9ccae0d5fb3e66e9dabc"
data-nc-root="https://www.neatcal.com/app"
data-booking-url="https://www.neatcal.com/app/services/booking/book?u=fcd3a2ae209d9ccae0d5fb3e66e9dabc&widget_order=all_services,time&photo_mode=0&click_and_go=1&class_list_tmpl=list-1"></script>
The easiest and native approach, not only in Gatsby, but generally in React is using the Helmet component. Basically, whatever is wrapped inside this component is outputted in the <head> tag, no matter the original component.
To use it, just install (via yarn add react-helmet or npm install --save react-helmet) it and use like:
import React from "react";
import {Helmet} from "react-helmet";
const HomePage = (props)=> {
return (
<div className="application">
<Helmet>
<meta charSet="utf-8" />
<title>My Title</title>
<link rel="canonical" href="http://someSite.net/example" />
<script id="nc-booking-widget-script" src="https://www.neatcal.com/app/services/booking/assets/js/integrate.js?v=21.04.26.5" data-access-key="fcd3a2ae209d9ccae0d5fb3e66e9dabc" data-nc-root="https://www.neatcal.com/app" data-booking-url="https://www.neatcal.com/app/services/booking/book?u=fcd3a2ae209d9ccae0d5fb3e66e9dabc&widget_order=all_services,time&photo_mode=0&click_and_go=1&class_list_tmpl=list-1"/>
</Helmet>
...
</div>
);
};
In Gatsby, in addition, you can customize the resultant HTML (created once build) by:
cp .cache/default-html.js src/html.js
This will copy the default-html.js file from .cache to src/html.js. There, you can embed your script like:
import React from "react"
import PropTypes from "prop-types"
import React from "react"
import PropTypes from "prop-types"
export default function HTML(props) {
return (
<html {...props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<script
id="nc-booking-widget-script"
src="https://www.neatcal.com/app/services/booking/assets/js/integrate.js?v=21.04.26.5"
data-access-key="fcd3a2ae209d9ccae0d5fb3e66e9dabc"
data-nc-root="https://www.neatcal.com/app"
data-booking-url="https://www.neatcal.com/app/services/booking/book?u=fcd3a2ae209d9ccae0d5fb3e66e9dabc&widget_order=all_services,time&photo_mode=0&click_and_go=1&class_list_tmpl=list-1"></script>
{props.headComponents}
</head>
<body {...props.bodyAttributes}>
{props.preBodyComponents}
<div
key={`body`}
id="___gatsby"
dangerouslySetInnerHTML={{ __html: props.body }}
/>
{props.postBodyComponents}
</body>
</html>
)
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array,
}
Both are valid and cleanest approaches.
I'm using Razzle for using React and Server Side Rendering with React Helmet. I have this problem when you use React Helmet to set meta tags with dynamic values, it's not displayed correctly. But it works if you set the meta tags with static values.
Please take a look at some codes.
SEO.js Component
import React, { Component } from 'react';
import { Helmet } from "react-helmet-async";
class SEO extends Component {
constructor(props) {
super(props);
this.state = {
title: this.props.title,
description: this.props.description,
image: this.props.image
}
}
shouldComponentUpdate(nextProps) {
if(this.props != nextProps) {
this.setState({
title: nextProps.title,
description: this.props.description,
image: nextProps.image
})
return true;
} else {
return false;
}
}
render() {
return (
<div>
<Helmet>
<title>{this.state.title ? this.state.title : "Volunteer Hub by Indorelawan"}</title>
<meta name="title" content={this.state.title ? this.state.title : "Volunteer Hub by Indorelawan"} />
<meta
name="description"
content={this.state.description ? this.state.description : "Volunteer Hub by Indorelawan adalah tempat kolaborasi antara relawan dan komunitas sosial yang memiliki semangat kerelawanan dan gotong royong untuk Indonesia."}
/>
<meta
property="og:title"
content={this.state.title ? this.state.title : "Volunteer Hub by Indorelawan"}
/>
<meta
property="og:description"
content={this.state.description ? this.state.description : "Volunteer Hub by Indorelawan adalah tempat kolaborasi antara relawan dan komunitas sosial yang memiliki semangat kerelawanan dan gotong royong untuk Indonesia."}
/>
<meta
property="og:image"
content={this.state.image ? this.state.image : "https://volunteerhub.id/assets/logo/seo.jpg"}
/>
<meta property="og:url" content="https://volunteerhub.id" />
<meta
name="twitter:title"
content={this.state.title ? this.state.title : "Volunteer Hub by Indorelawan"}
/>
<meta
name="twitter:description"
content={this.state.description ? this.state.description : "Volunteer Hub by Indorelawan adalah tempat kolaborasi antara relawan dan komunitas sosial yang memiliki semangat kerelawanan dan gotong royong untuk Indonesia."}
/>
<meta
name="twitter:image"
content={this.state.image ? this.state.image : "https://volunteerhub.id/assets/logo/seo.jpg"}
/>
<meta name="twitter:card" content="summary_large_image" />
</Helmet>
</div>
);
}
}
export default SEO;
Here is the example of setting up the static meta tags:
import React, {Component} from "react";
import SEO from "../../components/SEO";
class ScheduleContainer extends Component {
constructor(props) { super(props); }
render() {
return(
<div>
<SEO
title="Cek Jadwal | Volunteer Hub by Indorelawan"
description="Cek jadwal kegiatan di Volunteer Hub! Volunteer Hub by Indorelawan adalah tempat kolaborasi antara relawan dan komunitas sosial yang memiliki semangat kerelawanan dan gotong royong untuk Indonesia." />
</div>);
}
}
And here is the example of setting up the dynamic meta tags:
import React, {Component} from "react";
import axios from "axios";
import SEO from "../../components/SEO";
class EventContainer extends Component {
constructor(props) {
super(props);
this.state = {
event: {}
}
}
componentDidMount() {
axios.get('API_URL')
.then(response => {
this.setState({ event: response.data.result })
});
}
render() {
return(
<div>
<SEO
title={this.state.event.title}
description={this.state.event.description} />
</div>);
}
}
Server.js
import RootContainer from "./containers/RootContainer";
import React from "react";
import { StaticRouter } from "react-router-dom";
import express from "express";
import { renderToString } from "react-dom/server";
import { Helmet, HelmetProvider } from "react-helmet-async";
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
const server = express();
server
.disable("x-powered-by")
.use(express.static(process.env.RAZZLE_PUBLIC_DIR))
.get("/*", (req, res) => {
const context = {};
const helmetContext = {};
const markup = renderToString(
<HelmetProvider context={helmetContext}>
<StaticRouter context={context} location={req.url}>
<RootContainer />
</StaticRouter>
</HelmetProvider>
);
const { helmet } = helmetContext;
if (context.url) {
res.redirect(context.url);
} else {
res.status(200).send(
`<!doctype html>
<html lang="">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta
name="keywords"
content="volunteer, hub, by, indorelawan, volunteer hub, volunteer hub by indorelawan, kolaborasi, dimulai, dari, sini, ubah, niat, baik, jadi, aksi, baik, hari, ini"
/>
<meta name="robots" content="index, follow" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="language" content="Indonesia" />
<meta name="author" content="Indorelawan" />
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#222222" />
${helmet.title.toString()}
${helmet.meta.toString()}
${
assets.client.css
? `<link rel="stylesheet" href="${assets.client.css}">`
: ""
}
${
process.env.NODE_ENV === "production"
? `<script src="${assets.client.js}" defer></script>`
: `<script src="${
assets.client.js
}" defer crossorigin></script>`
}
...
</head>
<body>
<div id="root">${markup}</div>
<script>
if ("serviceWorker" in navigator) {
if (navigator.serviceWorker.controller) {
console.log("[PWA Builder] active service worker found, no need to register");
} else {
// Register the service worker
navigator.serviceWorker
.register("pwabuilder-sw.js", {
scope: "./"
})
.then(function (reg) {
console.log("[PWA Builder] Service worker has been registered for scope: " + reg.scope);
});
}
}
</script>
</body>
</html>`
);
}
});
export default server;
Now that you have seen the code, here is the result when I copy pasted to Google SERP Simulator and WhatsApp:
Static meta tags result from Schedule Page:
Google SERP Simulator
WhatsApp
Dynamic meta tags result from Event Page:
Google SERP Simulator
WhatsApp
From the result, it always return the default title and description tag not the title and description passed from the axios. Is is normal behavior or am I doing something wrong?
Razzle IS server side rendering, the problem with your dynamic meta tags scenario is that you're relying on data that is fetched in componentDidMount, and componentDidMount, as a lifecycle method from the commit phase, is not called on the server since there's no actual mounting on server side.
NextJS solves this problem for you because of getInitialProps, which is invoked on server and client.
Turns out Razzle is not Server Side Rendering. Either you have to define the SEO tags using a custom express server, or just use SSR for React.
I am using NextJS for this and this is a no problem.
I am trying to use the AutoComplete address service from Google Place API.
Found this library:
https://github.com/kenny-hibino/react-places-autocomplete#load-google-library
It asks for loading the library in my project:
https://github.com/kenny-hibino/react-places-autocomplete#getting-started
I would do it in the public/index.html if it's pure Reactjs project. However, the public/index.html in Gatsbyjs project will be deleted and re-generated every time when running:
Gatsby develop
command line.
How can I use the Google Place API in my Gatsbyjs project?
Update
I have tried 2 ways to achieve this.
Use React-Helmet in /layouts/index.js , here is how it looks like:
<Helmet>
<script src="https://maps.googleapis.com/maps/api/js?key={API}&libraries=places&callback=initAutocomplete" async defer></script>
</Helmet>
Put the script reference in the /public/index.html, which looks like this:
<!DOCTYPE html>
<html>
<head>
<meta charSet="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title data-react-helmet="true"></title>
<script src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key={API_KEY}&libraries=places" async defer ></script>
</head>
<body>
<div id="___gatsby"></div>
<script src="/commons.js"></script>
</body>
</html>
For the 1st solution, every time after I refresh my page, the project throws an error asking for loading the Google JavaScript Map API.
For the 2nd solution, every time after I re-start the Gatsby by the command line: gatsby develop
it re-generates the index.html which flushes away my JavaScript reference in it.
You shouldn't modify any files in the public forlder with GatsbyJS.
Instead, I recommend you to customize your html.js file.
To do so, first run:
cp .cache/default-html.js src/html.js
You should have the html.js file in /src/html.js.
Now you can put your <script> tag within the <head>.
Update Feb 24, 2020
Here's a more modern implementation using React hooks with some performance optimizations based on React.memo and a custom shouldUpdate function. See this blog post for details.
import { functions, isEqual, omit } from 'lodash'
import React, { useState, useEffect, useRef } from 'react'
function Map({ options, onMount, className, onMountProps }) {
const ref = useRef()
const [map, setMap] = useState()
useEffect(() => {
// The Map constructor modifies its options object in place by adding
// a mapTypeId with default value 'roadmap'. This confuses shouldNotUpdate.
// { ...options } prevents this by passing in a copy.
const onLoad = () =>
setMap(new window.google.maps.Map(ref.current, { ...options }))
if (!window.google) {
const script = document.createElement(`script`)
script.src = `https://maps.googleapis.com/maps/api/js?key=` + YOUR_API_KEY
document.head.append(script)
script.addEventListener(`load`, onLoad)
return () => script.removeEventListener(`load`, onLoad)
} else onLoad()
}, [options])
if (map && typeof onMount === `function`) onMount(map, onMountProps)
return (
<div
style={{ height: `60vh`, margin: ` 1em 0`, borderRadius: ` 0.5em` }}
{...{ ref, className }}
/>
)
}
function shouldNotUpdate(props, nextProps) {
const [funcs, nextFuncs] = [functions(props), functions(nextProps)]
const noPropChange = isEqual(omit(props, funcs), omit(nextProps, nextFuncs))
const noFuncChange =
funcs.length === nextFuncs.length &&
funcs.every(fn => props[fn].toString() === nextProps[fn].toString())
return noPropChange && noFuncChange
}
export default React.memo(Map, shouldNotUpdate)
Map.defaultProps = {
options: {
center: { lat: 48, lng: 8 },
zoom: 5,
},
}
Old Answer
Using html.js
Modifying src/html.js like so (as Nenu suggests) is one option.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class HTML extends Component {
render() {
return (
<html {...this.props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{this.props.headComponents}
</head>
<body {...this.props.bodyAttributes}>
{this.props.preBodyComponents}
<div
key={`body`}
id="___gatsby"
dangerouslySetInnerHTML={{ __html: this.props.body }}
/>
{this.props.postBodyComponents}
// MODIFICATION // ===================
<script
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"
async
defer
/>
// ===================
</body>
</html>
)
}
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array,
}
Then you can access the Google Maps API anywhere in your project from window.google.maps.(Map|Marker|etc.).
The React way
To me that felt a little anachronistic, though. If you want a reusable React component that you can import into any page or template as import Map from './Map', I suggest this instead. (Hint: See update below for equivalent function component.)
// src/components/Map.js
import React, { Component } from 'react'
export default class Map extends Component {
onLoad = () => {
const map = new window.google.maps.Map(
document.getElementById(this.props.id),
this.props.options
)
this.props.onMount(map)
}
componentDidMount() {
if (!window.google) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = `https://maps.google.com/maps/api/js?key=YOUR_API_KEY`
const headScript = document.getElementsByTagName('script')[0]
headScript.parentNode.insertBefore(script, headScript)
script.addEventListener('load', () => {
this.onLoad()
})
} else {
this.onLoad()
}
}
render() {
return <div style={{ height: `50vh` }} id={this.props.id} />
}
}
Use it like so:
// src/pages/contact.js
import React from 'react'
import Map from '../components/Map'
const center = { lat: 50, lng: 10 }
const mapProps = {
options: {
center,
zoom: 8,
},
onMount: map => {
new window.google.maps.Marker({
position: center,
map,
title: 'Europe',
})
},
}
export default function Contact() {
return (
<>
<h1>Contact</h1>
<Map id="contactMap" {...mapProps} />
</>
)
}
What woked for me was to create a gatsby-ssr.js file in the root of my project, and then include the script there, like this:
import React from "react"
export function onRenderBody({ setHeadComponents }) {
setHeadComponents([
<script
key="abc"
type="text/javascript"
src={`https://maps.googleapis.com/maps/api/js?key=${process.env.GATSBY_API_KEY}&libraries=places`}
/>,
])
}
Don't forget to include GATSBY_API_KEY or whatever you want to call it in your .env.development and .env.production files:
GATSBY_API_KEY=...