+
+## 🚀 Quick start
+
+1. Setup Airtable
+
+ Create a new base named `Submissions` and create a table with three columns, "Name", "Email", and "Message".
+
+2. **Get Airtable Credentials.**
+
+ There are **2** environment variable you'll need to add your project to properly run the example:
+
+ - `AIRTABLE_KEY`: Get Airtable API Key. [Airtable Docs](https://support.airtable.com/hc/en-us/articles/219046777-How-do-I-get-my-API-key-)
+ - `AIRTABLE_DB`: Get the ID for the "Submissions" Base in interactive Airtable API docs. [Airtable Docs](https://airtable.com/api)
+
+ You'll want to add these as environment variables when deploying to Gatsby Cloud. Don't forget to add them to the Preview variables if you plan to add a CMS preview integration.
+
+3. **Start developing.**
+
+ To get started, run `yarn` to add all necessary packages.
+
+ When developing locally, you'll want to include the ENV variables in your `.env.development`. Read more about how Gatsby handles `.env` files and environment variables in the [Gatbsy Docs](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/)
+
+ ```shell
+ cd airtable-form
+ yarn
+ yarn run develop
+ ```
+
+4. **Open the code and start customizing!**
+
+ Your site is now running at http://localhost:8000! You can use the UI on the index page to test the functions or directly access them at http://localhost:8000/api/airtable
+
+ For this route, hitting the route with a POST request with the following body should submit a form response to your Airtable base:
+
+ ```json
+ {
+ "name": "Sample Name",
+ "email": "sample@example.com",
+ "message": "Hello, World!"
+ }
+ ```
+
+ Edit `src/pages/index.js` to see your site update in real-time!
+
+5. **Deploy**
+
+You can deploy this example on Gatsby Cloud by copying the example into a new repo and [connecting that to Gatsby Cloud](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/deploying-to-gatsby-cloud/#set-up-an-existing-gatsby-site).
+
+
diff --git a/examples/functions-airtable-form/gatsby-config.js b/examples/functions-airtable-form/gatsby-config.js
new file mode 100644
index 0000000000000..b2dc0496eab81
--- /dev/null
+++ b/examples/functions-airtable-form/gatsby-config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ flags: {
+ FUNCTIONS: true,
+ },
+ siteMetadata: {
+ title: "Airtable Form",
+ },
+ plugins: ["gatsby-plugin-gatsby-cloud"],
+}
diff --git a/examples/functions-airtable-form/package.json b/examples/functions-airtable-form/package.json
new file mode 100644
index 0000000000000..6e4393c66219d
--- /dev/null
+++ b/examples/functions-airtable-form/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "airtable-form",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Airtable Form",
+ "author": "Joel Smith",
+ "keywords": [
+ "gatsby"
+ ],
+ "scripts": {
+ "develop": "gatsby develop",
+ "start": "gatsby develop",
+ "build": "gatsby build",
+ "serve": "gatsby serve",
+ "clean": "gatsby clean"
+ },
+ "dependencies": {
+ "airtable": "^0.10.1",
+ "gatsby": "^3.4.0",
+ "gatsby-plugin-gatsby-cloud": "^2.3.0",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1"
+ }
+}
diff --git a/examples/functions-airtable-form/src/api/airtable.js b/examples/functions-airtable-form/src/api/airtable.js
new file mode 100644
index 0000000000000..eeae50b682942
--- /dev/null
+++ b/examples/functions-airtable-form/src/api/airtable.js
@@ -0,0 +1,51 @@
+const Airtable = require("airtable")
+
+Airtable.configure({
+ endpointUrl: "https://api.airtable.com",
+ //Your API Key from Airtable
+ apiKey: process.env.AIRTABLE_KEY,
+})
+
+// Your Table ID from Airtable
+const db = Airtable.base(process.env.AIRTABLE_DB)
+
+const handler = (req, res) => {
+ try {
+ if (req.method !== "POST") {
+ return res.status(404).json({ message: "This endpoint requires a POST" })
+ }
+
+ const data = req.body
+
+ if (!data) {
+ return res.status(500).json({ error: "There isn't any data." })
+ }
+
+ db("Submissions").create(
+ [
+ {
+ fields: {
+ Name: data.name,
+ Email: data.email,
+ Message: data.message,
+ },
+ },
+ ],
+ (err, records) => {
+ if (err) {
+ res.json({
+ message: "Error adding record to Airtable.",
+ error: err.message,
+ })
+ } else {
+ res.json({ message: `Successfully submitted message` })
+ }
+ }
+ )
+ } catch (err) {
+ console.log(err)
+ res.json({ message: "There has been a big error.", error: err })
+ }
+}
+
+module.exports = handler
diff --git a/examples/functions-airtable-form/src/images/icon.png b/examples/functions-airtable-form/src/images/icon.png
new file mode 100644
index 0000000000000..38b2fb0e467e0
Binary files /dev/null and b/examples/functions-airtable-form/src/images/icon.png differ
diff --git a/examples/functions-airtable-form/src/pages/404.js b/examples/functions-airtable-form/src/pages/404.js
new file mode 100644
index 0000000000000..053ae0e831ee9
--- /dev/null
+++ b/examples/functions-airtable-form/src/pages/404.js
@@ -0,0 +1,54 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+// styles
+const pageStyles = {
+ color: "#232129",
+ padding: "96px",
+ fontFamily: "-apple-system, Roboto, sans-serif, serif",
+}
+const headingStyles = {
+ marginTop: 0,
+ marginBottom: 64,
+ maxWidth: 320,
+}
+
+const paragraphStyles = {
+ marginBottom: 48,
+}
+const codeStyles = {
+ color: "#8A6534",
+ padding: 4,
+ backgroundColor: "#FFF4DB",
+ fontSize: "1.25rem",
+ borderRadius: 4,
+}
+
+// markup
+const NotFoundPage = () => {
+ return (
+
+ Not found
+
Page not found
+
+ Sorry{" "}
+
+ 😔
+ {" "}
+ we couldn’t find what you were looking for.
+
+ {process.env.NODE_ENV === "development" ? (
+ <>
+
+ Try creating a page in src/pages/.
+
+ >
+ ) : null}
+
+ Go home.
+
+
+ )
+}
+
+export default NotFoundPage
diff --git a/examples/functions-airtable-form/src/pages/index.js b/examples/functions-airtable-form/src/pages/index.js
new file mode 100644
index 0000000000000..d3e73fb0f5f7d
--- /dev/null
+++ b/examples/functions-airtable-form/src/pages/index.js
@@ -0,0 +1,36 @@
+import * as React from "react"
+
+export default function AirtableUI() {
+ return (
+
+ )
+}
diff --git a/examples/functions-auth0/.env.template b/examples/functions-auth0/.env.template
new file mode 100644
index 0000000000000..559ff22d5d56d
--- /dev/null
+++ b/examples/functions-auth0/.env.template
@@ -0,0 +1,6 @@
+JWT_ISSUER=https://domain-from-auth0-application.com
+JWT_AUDIENCE=https://api/tv-shows
+GATSBY_AUTH0_DOMAIN=domain-from-auth0-application.com
+GATSBY_AUTH0_CLIENT_ID=Copy from Auth0 application
+GATSBY_AUTH0_AUDIENCE=https://api/tv-shows
+GATSBY_AUTH0_SCOPE=openid profile read:shows
diff --git a/examples/functions-auth0/LICENSE b/examples/functions-auth0/LICENSE
new file mode 100644
index 0000000000000..7e964c1ee5f4f
--- /dev/null
+++ b/examples/functions-auth0/LICENSE
@@ -0,0 +1,14 @@
+The BSD Zero Clause License (0BSD)
+
+Copyright (c) 2020 Gatsby Inc.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/examples/functions-auth0/README.md b/examples/functions-auth0/README.md
new file mode 100644
index 0000000000000..b769b9a9b1787
--- /dev/null
+++ b/examples/functions-auth0/README.md
@@ -0,0 +1,55 @@
+# gatsby-auth0-functions
+
+This example shows how to use [@serverless-jwt](https://github.com/sandrinodimattia/serverless-jwt) with Gatsby and Gatsby Hosted Functions.
+
+## Inspiration
+
+This code was inspired by this [article](https://sandrino.dev/blog/securing-netlify-functions-with-serverless-jwt) and [repo](https://github.com/sandrinodimattia/serverless-jwt).
+
+## 🚀 Quick start
+
+To test this Example locally you'll need to:
+
+1. Create an application in [Auth0](https://auth0.com/) of type "Single Page Application" and configure `http://localhost:8000` as the Allowed Callback URL, Allowed Logout URL, Allowed Web Origins and Allowed CORS.
+2. Create an API in Auth0 (e.g. with the name of `tv-shows` and identifier of `https://api/tv-shows`) and create a permission for that API (e.g. `read:shows`)
+3. Rename the `.env-template` file to `.env.development` and update all of the settings there with the domain and clientId from the appplication you made in Auth0.
+4. Run `yarn run start` which will run the Gatsby application and the Gatsby functions.
+
+## How does example this work?
+
+### Gatsby
+
+The Gatsby application uses [@auth0/auth0-react](https://github.com/auth0/auth0-react) to authenticate the user. Once the user is authenticated, the Gatsby application will receive an `id_token` and `access_token` from Auth0;
+
+The `access_token` is then provided to our Functions to authenticate the request.
+
+### Gatsby Functions
+
+In the Gatsby Functions we use [@serverless-jwt/jwt-verifier](https://github.com/sandrinodimattia/serverless-jwt/tree/master/packages/jwt-verifier) to secure our functions.
+
+The `JwtVerifier` serves as a way to verify your token. If the token is not valid, the we return an error to the client. If it is valid, it will expose all of the claims to the current function and you'll have the guarantee that the request is authenticated.
+
+```js
+const {
+ JwtVerifier,
+ getTokenFromHeader,
+} = require("@serverless-jwt/jwt-verifier")
+
+const jwt = new JwtVerifier({
+ issuer: process.env.JWT_ISSUER,
+ audience: process.env.JWT_AUDIENCE,
+})
+
+const shows = async (req, res) => {
+ const scope = "read:shows"
+ const token = getTokenFromHeader(req.get("authorization"))
+ const claims = await jwt.verifyAccessToken(token)
+
+ if (!claims || !claims.scope || claims.scope.indexOf(scope) === -1) {
+ return res.status(403).json({
+ error: "access_denied",
+ error_description: `Token does not contain the required '${scope}' scope`,
+ })
+ }
+}
+```
diff --git a/examples/functions-auth0/gatsby-browser.js b/examples/functions-auth0/gatsby-browser.js
new file mode 100644
index 0000000000000..3ef06a4448696
--- /dev/null
+++ b/examples/functions-auth0/gatsby-browser.js
@@ -0,0 +1,22 @@
+import React from "react"
+import { navigate } from "gatsby"
+import { Auth0Provider } from "@auth0/auth0-react"
+
+import "./src/styles/site.css"
+
+const onRedirectCallback = appState => navigate(appState?.returnTo || "/")
+
+export const wrapRootElement = ({ element }) => {
+ return (
+
+ {element}
+
+ )
+}
diff --git a/examples/functions-auth0/gatsby-config.js b/examples/functions-auth0/gatsby-config.js
new file mode 100644
index 0000000000000..dd306b3999ee5
--- /dev/null
+++ b/examples/functions-auth0/gatsby-config.js
@@ -0,0 +1,20 @@
+const activeEnv =
+ process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV || "development"
+
+require("dotenv").config({
+ path: `.env.${activeEnv}`,
+})
+
+module.exports = {
+ flags: {
+ FUNCTIONS: true,
+ },
+ plugins: [
+ {
+ resolve: "gatsby-plugin-create-client-paths",
+ options: { prefixes: ["/*"] },
+ },
+ `gatsby-plugin-postcss`,
+ `gatsby-plugin-gatsby-cloud`,
+ ],
+}
diff --git a/examples/functions-auth0/package.json b/examples/functions-auth0/package.json
new file mode 100644
index 0000000000000..b47c2a0024202
--- /dev/null
+++ b/examples/functions-auth0/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "gatsby-auth0-functions",
+ "private": true,
+ "scripts": {
+ "build": "gatsby build",
+ "develop": "gatsby develop",
+ "start": "npm run develop",
+ "serve": "gatsby serve",
+ "clean": "gatsby clean"
+ },
+ "dependencies": {
+ "@auth0/auth0-react": "^1.0.0",
+ "@serverless-jwt/jwt-verifier": "^0.2.1",
+ "dotenv": "^8.2.0",
+ "gatsby": "^3.4.0-next.6",
+ "gatsby-plugin-gatsby-cloud": "^2.3.0",
+ "node-fetch": "^2.6.1",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "devDependencies": {
+ "gatsby-plugin-create-client-paths": "^3.3.0",
+ "gatsby-plugin-postcss": "^4.3.0",
+ "prettier": "^2.2.1",
+ "tailwindcss": "^1.5.2"
+ }
+}
diff --git a/examples/functions-auth0/postcss.config.js b/examples/functions-auth0/postcss.config.js
new file mode 100644
index 0000000000000..077422c4d1038
--- /dev/null
+++ b/examples/functions-auth0/postcss.config.js
@@ -0,0 +1,3 @@
+module.exports = () => ({
+ plugins: [require("tailwindcss")],
+})
diff --git a/examples/functions-auth0/src/api/me.js b/examples/functions-auth0/src/api/me.js
new file mode 100644
index 0000000000000..ab4e4be418e20
--- /dev/null
+++ b/examples/functions-auth0/src/api/me.js
@@ -0,0 +1,33 @@
+const {
+ JwtVerifier,
+ JwtVerifierError,
+ getTokenFromHeader,
+} = require("@serverless-jwt/jwt-verifier")
+
+const jwt = new JwtVerifier({
+ issuer: process.env.JWT_ISSUER,
+ audience: process.env.JWT_AUDIENCE,
+})
+
+const me = async (req, res) => {
+ if (req.method !== "GET") {
+ return res.json({ message: "Try a GET!" })
+ }
+
+ try {
+ const token = getTokenFromHeader(req.get("authorization"))
+ const claims = await jwt.verifyAccessToken(token)
+
+ res.status(200).json({ claims })
+ } catch (err) {
+ if (err instanceof JwtVerifierError) {
+ console.error(err.code, err.message)
+ res.status(401).json({ error_description: err.message })
+ } else {
+ console.error(err)
+ res.status(500).json({ error_description: err.message })
+ }
+ }
+}
+
+export default me
diff --git a/examples/functions-auth0/src/api/shows.js b/examples/functions-auth0/src/api/shows.js
new file mode 100644
index 0000000000000..608d73daa7db7
--- /dev/null
+++ b/examples/functions-auth0/src/api/shows.js
@@ -0,0 +1,40 @@
+const fetch = require("node-fetch").default
+const {
+ JwtVerifier,
+ getTokenFromHeader,
+} = require("@serverless-jwt/jwt-verifier")
+
+const jwt = new JwtVerifier({
+ issuer: process.env.JWT_ISSUER,
+ audience: process.env.JWT_AUDIENCE,
+})
+
+const shows = async (req, res) => {
+ try {
+ const scope = "read:shows"
+ const token = getTokenFromHeader(req.get("authorization"))
+ const claims = await jwt.verifyAccessToken(token)
+
+ if (!claims || !claims.scope || claims.scope.indexOf(scope) === -1) {
+ return res.status(403).json({
+ error: "access_denied",
+ error_description: `Token does not contain the required '${scope}' scope`,
+ })
+ }
+
+ const result = await fetch("https://api.tvmaze.com/shows")
+ const shows = await result.json()
+
+ res.status(200).json(
+ shows.map(s => ({
+ id: s.id,
+ url: s.url,
+ name: s.name,
+ }))
+ )
+ } catch (err) {
+ res.status(500).json({ error_description: err.message })
+ }
+}
+
+export default shows
diff --git a/examples/functions-auth0/src/components/header.js b/examples/functions-auth0/src/components/header.js
new file mode 100644
index 0000000000000..03c61b5667b91
--- /dev/null
+++ b/examples/functions-auth0/src/components/header.js
@@ -0,0 +1,54 @@
+import React from "react"
+import { useAuth0 } from "@auth0/auth0-react"
+
+const Header = () => {
+ const { isAuthenticated, loginWithRedirect, logout } = useAuth0()
+
+ return (
+
+
+ Before your user can call an API, they need to authenticate. Go
+ ahead and click the Login
+ {` `}
+ button on the top right.
+
+ ) : (
+
+ You are now signed in. To sign out, click the{" "}
+ Logout button on the top right.
+
+ )}
+
+ {JSON.stringify({ isLoading, error, user }, null, 2)}
+
+
Calling a Gatsby Hosted Function
+
+ Once the user is authenticated, you can call the API. If you try this
+ without authenticating, you'll get an error.
+
+
+
+ Requires a valid access_token.
+
+
+
+ Requires a valid access_token with the read:shows scope.
+
+ {response && (
+
+ {JSON.stringify(response, null, 2)}
+
+ )}
+
+
+ )
+}
diff --git a/examples/functions-auth0/src/styles/site.css b/examples/functions-auth0/src/styles/site.css
new file mode 100644
index 0000000000000..b5c61c956711f
--- /dev/null
+++ b/examples/functions-auth0/src/styles/site.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/examples/functions-auth0/static/favicon.ico b/examples/functions-auth0/static/favicon.ico
new file mode 100644
index 0000000000000..1a466ba8852cf
Binary files /dev/null and b/examples/functions-auth0/static/favicon.ico differ
diff --git a/examples/functions-basic-form/.gitignore b/examples/functions-basic-form/.gitignore
new file mode 100644
index 0000000000000..557f97c6feb55
--- /dev/null
+++ b/examples/functions-basic-form/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+.cache/
+public
diff --git a/examples/functions-basic-form/README.md b/examples/functions-basic-form/README.md
new file mode 100644
index 0000000000000..59ffb7251a1a9
--- /dev/null
+++ b/examples/functions-basic-form/README.md
@@ -0,0 +1,23 @@
+# Gatsby basic form Functions example
+
+This example shows how to build a form with [react-hook-form](https://react-hook-form.com/) that submits to a Gatsby Function.
+
+1. **Start developing.**
+
+ To get started clone this repo locally and run `yarn` to add all necessary packages.
+
+ ```shell
+ cd basic-form
+ yarn
+ yarn run develop
+ ```
+
+2. **Open the code and start customizing!**
+
+ Your site is now running at http://localhost:8000! You can use the UI on the index page to test the functions or directly access them at http://localhost:8000/api/form
+
+ Try editing the function in `src/api/form.ts` or form at `src/pages/index.js`
+
+3. **Deploy**
+
+You can deploy this example on Gatsby Cloud by copying the example into a new repo and [connecting that to Gatsby Cloud](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/deploying-to-gatsby-cloud/#set-up-an-existing-gatsby-site).
diff --git a/examples/functions-basic-form/gatsby-config.js b/examples/functions-basic-form/gatsby-config.js
new file mode 100644
index 0000000000000..06783fc1a5af8
--- /dev/null
+++ b/examples/functions-basic-form/gatsby-config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ flags: {
+ FUNCTIONS: true,
+ },
+ siteMetadata: {
+ title: "form",
+ },
+ plugins: [],
+}
diff --git a/examples/functions-basic-form/package.json b/examples/functions-basic-form/package.json
new file mode 100644
index 0000000000000..710a3ae8ca810
--- /dev/null
+++ b/examples/functions-basic-form/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "form",
+ "version": "1.0.0",
+ "private": true,
+ "description": "form",
+ "author": "Kyle Mathews",
+ "keywords": [
+ "gatsby"
+ ],
+ "scripts": {
+ "develop": "gatsby develop",
+ "start": "gatsby develop",
+ "build": "gatsby build",
+ "serve": "gatsby serve",
+ "clean": "gatsby clean"
+ },
+ "dependencies": {
+ "gatsby": "^3.4.1",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1",
+ "react-hook-form": "^7.5.2"
+ }
+}
diff --git a/examples/functions-basic-form/src/api/form.ts b/examples/functions-basic-form/src/api/form.ts
new file mode 100644
index 0000000000000..1af02108f8194
--- /dev/null
+++ b/examples/functions-basic-form/src/api/form.ts
@@ -0,0 +1,9 @@
+import { GatsbyFunctionRequest, GatsbyFunctionResponse } from "gatsby"
+
+export default function handler(
+ req: GatsbyFunctionRequest,
+ res: GatsbyFunctionResponse
+) {
+ console.log(`submitted form`, req.body)
+ res.json(`ok`)
+}
diff --git a/examples/functions-basic-form/src/pages/404.js b/examples/functions-basic-form/src/pages/404.js
new file mode 100644
index 0000000000000..053ae0e831ee9
--- /dev/null
+++ b/examples/functions-basic-form/src/pages/404.js
@@ -0,0 +1,54 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+// styles
+const pageStyles = {
+ color: "#232129",
+ padding: "96px",
+ fontFamily: "-apple-system, Roboto, sans-serif, serif",
+}
+const headingStyles = {
+ marginTop: 0,
+ marginBottom: 64,
+ maxWidth: 320,
+}
+
+const paragraphStyles = {
+ marginBottom: 48,
+}
+const codeStyles = {
+ color: "#8A6534",
+ padding: 4,
+ backgroundColor: "#FFF4DB",
+ fontSize: "1.25rem",
+ borderRadius: 4,
+}
+
+// markup
+const NotFoundPage = () => {
+ return (
+
+ Not found
+
Page not found
+
+ Sorry{" "}
+
+ 😔
+ {" "}
+ we couldn’t find what you were looking for.
+
+ {process.env.NODE_ENV === "development" ? (
+ <>
+
+ Try creating a page in src/pages/.
+
+ >
+ ) : null}
+
+ Go home.
+
+
+ )
+}
+
+export default NotFoundPage
diff --git a/examples/functions-basic-form/src/pages/index.js b/examples/functions-basic-form/src/pages/index.js
new file mode 100644
index 0000000000000..1a00451515ea6
--- /dev/null
+++ b/examples/functions-basic-form/src/pages/index.js
@@ -0,0 +1,81 @@
+import React from "react"
+import { useForm } from "react-hook-form"
+
+export default function App() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm()
+ const onSubmit = data => {
+ fetch(`/api/form`, {
+ method: `POST`,
+ body: JSON.stringify(data),
+ headers: {
+ "content-type": `application/json`,
+ },
+ })
+ .then(res => res.json())
+ .then(body => {
+ console.log(`response from API:`, body)
+ })
+ }
+
+ console.log({ errors })
+
+ return (
+
+ )
+}
diff --git a/examples/functions-google-auth/.env.development b/examples/functions-google-auth/.env.development
new file mode 100644
index 0000000000000..475e8cefdcf26
--- /dev/null
+++ b/examples/functions-google-auth/.env.development
@@ -0,0 +1,2 @@
+GATSBY_GOOGLE_CLIENT_ID=955665132496-jr4vop70ersipvsqbgij7i7d8anq3lrl.apps.googleusercontent.com
+CLIENT_SECRET=L3_d5JlcR3SXk1hW1EJwOce3
\ No newline at end of file
diff --git a/examples/functions-google-auth/LICENSE b/examples/functions-google-auth/LICENSE
new file mode 100644
index 0000000000000..23eb9b6463403
--- /dev/null
+++ b/examples/functions-google-auth/LICENSE
@@ -0,0 +1,14 @@
+The BSD Zero Clause License (0BSD)
+
+Copyright (c) 2020 Gatsby Inc.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
\ No newline at end of file
diff --git a/examples/functions-google-auth/README.md b/examples/functions-google-auth/README.md
new file mode 100644
index 0000000000000..8a54cdafb391d
--- /dev/null
+++ b/examples/functions-google-auth/README.md
@@ -0,0 +1,59 @@
+
+ )
+}
+
+export default IndexPage
\ No newline at end of file
diff --git a/examples/functions-google-sheets/.gitignore b/examples/functions-google-sheets/.gitignore
new file mode 100644
index 0000000000000..d694c455accc5
--- /dev/null
+++ b/examples/functions-google-sheets/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.cache/
+public
+config/
\ No newline at end of file
diff --git a/examples/functions-google-sheets/LICENSE b/examples/functions-google-sheets/LICENSE
new file mode 100644
index 0000000000000..23eb9b6463403
--- /dev/null
+++ b/examples/functions-google-sheets/LICENSE
@@ -0,0 +1,14 @@
+The BSD Zero Clause License (0BSD)
+
+Copyright (c) 2020 Gatsby Inc.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
\ No newline at end of file
diff --git a/examples/functions-google-sheets/README.md b/examples/functions-google-sheets/README.md
new file mode 100644
index 0000000000000..90ae730be4080
--- /dev/null
+++ b/examples/functions-google-sheets/README.md
@@ -0,0 +1,64 @@
+
+
+## 🚀 Quick start
+
+1. **Get Google Authentication Token.**
+
+ This sample uses the Google Service account to authenticate into Google Spreadsheet API. To receive the proper token information, you'll need to set up an account in the [Google API Console](https://console.cloud.google.com/apis/dashboard).
+
+ - Set up your project in the Google API console. You can follow the instructions highlighted in Google Account Authentication tutorial [here](https://theoephraim.github.io/node-google-sreadsheet/#/getting-started/authentication)
+ - Generate a service account and download the access token. You can follow the steps highlighted in [Account Authentication - Service Account](https://theoephraim.github.io/node-google-spreadsheet/#/getting-started/authentication?id=service-account)
+ - Create an `.env.development` file. Copy into it from the downloaded JSON key file:
+
+ ```
+ GOOGLE_SERVICE_ACCOUNT_EMAIL=copy service account email
+ GOOGLE_PRIVATE_KEY=copy private key
+ ```
+
+2. **Create a Test Spreadsheet.**
+
+ - Generate a new spreadsheet via [Google Sheets](https://docs.google.com/spreadsheets)
+ - Grant owner access to the spreadsheet to your service account email address.
+ - You will need the Sheet ID to propertly run the example. Sheet ID can be found in the URL of a Google spreadsheet. For example, you can find it via" `https://docs.google.com/spreadsheets/d//`. Add this to your `.env.development` file as well.
+
+3. **Start developing.**
+
+ To get started, run `yarn` to add all necessary packages.
+
+ When developing locally, you'll want to include the ENV variables in your `.env.development`. Read more about how Gatsby handles `.env` files and environment variables in the [Gatbsy Docs](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/)
+
+ ```shell
+ cd google-sheets
+ yarn
+ yarn run develop
+ ```
+
+4. **Open the code and start customizing!**
+
+ Your site is now running at http://localhost:8000! You can use the UI on the index page to test the functions or directly access them at http://localhost:8000/api/sheets
+
+ Edit `src/pages/index.js` to see your site update in real-time!
+
+5. **Deploy**
+
+You can deploy this example on Gatsby Cloud by copying the example into a new repo and [connecting that to Gatsby Cloud](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/deploying-to-gatsby-cloud/#set-up-an-existing-gatsby-site).
+
+
diff --git a/examples/functions-google-sheets/gatsby-config.js b/examples/functions-google-sheets/gatsby-config.js
new file mode 100644
index 0000000000000..34786bada49e4
--- /dev/null
+++ b/examples/functions-google-sheets/gatsby-config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ flags: {
+ FUNCTIONS: true,
+ },
+ siteMetadata: {
+ title: "Google Sheets",
+ },
+ plugins: ["gatsby-plugin-gatsby-cloud"],
+}
diff --git a/examples/functions-google-sheets/package.json b/examples/functions-google-sheets/package.json
new file mode 100644
index 0000000000000..97038756258e4
--- /dev/null
+++ b/examples/functions-google-sheets/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "google-sheets",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Google Spreadsheets",
+ "author": "Polina Vorozheykina",
+ "keywords": [
+ "gatsby"
+ ],
+ "scripts": {
+ "develop": "gatsby develop",
+ "start": "gatsby develop",
+ "build": "gatsby build",
+ "serve": "gatsby serve",
+ "clean": "gatsby clean"
+ },
+ "dependencies": {
+ "gatsby": "^3.4.1",
+ "gatsby-plugin-gatsby-cloud": "^2.3.0",
+ "google-spreadsheet": "^3.1.15",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1"
+ }
+}
diff --git a/examples/functions-google-sheets/src/api/sheets.js b/examples/functions-google-sheets/src/api/sheets.js
new file mode 100644
index 0000000000000..a90ce7cefb18b
--- /dev/null
+++ b/examples/functions-google-sheets/src/api/sheets.js
@@ -0,0 +1,59 @@
+const { GoogleSpreadsheet } = require("google-spreadsheet")
+
+const handler = async (req, res) => {
+ try {
+ if (req.method !== "POST") {
+ res.json({ message: "Try a POST!" })
+ }
+
+ const doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEET_ID)
+
+ await doc.useServiceAccountAuth({
+ client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
+ private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/gm, "\n"),
+ })
+ await doc.getInfo()
+
+ // Always use the first sheet.
+ const sheet = doc.sheetsByIndex[0]
+
+ // If not set yet (sheet is empty), add the headers.
+ if (!sheet.headerValues) {
+ await sheet.setHeaderRow([`name`, `snack`, `drink`])
+ }
+
+ var count = 0
+ var rows = []
+ for (element in req.body) {
+ if (element == "name" + count) {
+ rows.push({
+ name: req.body[element],
+ snack: req.body["snack" + count],
+ drink: req.body["drink" + count],
+ })
+ count++
+ }
+ }
+
+ return await sheet.addRows(rows).then(
+ value => {
+ return res.status(200).json({
+ message: "Pushing rows to new sheet",
+ })
+ },
+ error => {
+ console.error(error)
+ if (error.response) {
+ return res.status(500).json({
+ error: error.response,
+ })
+ }
+ }
+ )
+ } catch (err) {
+ console.log(err)
+ return res.status(500).json({ message: "There was an error", error: err })
+ }
+}
+
+module.exports = handler
diff --git a/examples/functions-google-sheets/src/images/icon.png b/examples/functions-google-sheets/src/images/icon.png
new file mode 100644
index 0000000000000..38b2fb0e467e0
Binary files /dev/null and b/examples/functions-google-sheets/src/images/icon.png differ
diff --git a/examples/functions-google-sheets/src/models/columns.js b/examples/functions-google-sheets/src/models/columns.js
new file mode 100644
index 0000000000000..7d6c22b742a6f
--- /dev/null
+++ b/examples/functions-google-sheets/src/models/columns.js
@@ -0,0 +1,3 @@
+const Columns = Object.freeze({ name: "name", snack: "snack", drink: "drink" })
+
+export default Columns
diff --git a/examples/functions-google-sheets/src/models/spreadsheet-row.js b/examples/functions-google-sheets/src/models/spreadsheet-row.js
new file mode 100644
index 0000000000000..83ccc93f0deae
--- /dev/null
+++ b/examples/functions-google-sheets/src/models/spreadsheet-row.js
@@ -0,0 +1,48 @@
+import React from "react"
+import PropTypes from "prop-types"
+import Columns from "./columns"
+
+const SpreadsheetsRow = ({ idx, state, handleChange }) => {
+ const nameId = `${Columns.name}${idx}`
+ const snackId = `${Columns.snack}${idx}`
+ const drinkId = `${Columns.drink}${idx}`
+ return (
+
+
+## 🚀 Quick start
+
+1. **Get SendGrid Credentials.**
+
+ Create an account on [SendGrid](https://sendgrid.com/) and verify a "single sender" email address that the function will use for sending emails.
+
+ Add the following **2** environment variables to your a file named `.env.development`. You'll need these for the function to be able to send emails:
+
+ - `SENDGRID_API_KEY`: An SendGrid API Key with full access. [SendGrid Docs](https://sendgrid.com/docs/ui/account-and-settings/api-keys/)
+ - `SENDGRID_AUTHORIZED_EMAIL`: the "single sender" the email address you verified with SendGrid [SendGrid Docs](https://sendgrid.com/docs/glossary/sender-authentication/)
+
+ Read more about how Gatsby handles `.env` files and environment variables in the [Gatbsy Docs](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/)
+
+ You'll also want to add these as environment variables when deploying to Gatsby Cloud. Don't forget to add them to the Preview variables if you plan to add a CMS preview integration.
+
+2. **Start developing.**
+
+ To get started, run `yarn` to install all necessary packages.
+
+ ```shell
+ cd sendgrid-email
+ yarn
+ yarn run develop
+ ```
+
+3. **Open the code and start customizing!**
+
+ Your site is now running at http://localhost:8000! You can use the UI on the index page to test the functions or directly access them at http://localhost:8000/api/sendgrid
+
+ Edit `src/pages/index.js` to see your site update in real-time!
+
+4. **Deploy**
+
+You can deploy this example on Gatsby Cloud by copying the example into a new repo and [connecting that to Gatsby Cloud](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/deploying-to-gatsby-cloud/#set-up-an-existing-gatsby-site).
+
+
diff --git a/examples/functions-sendgrid-email/gatsby-config.js b/examples/functions-sendgrid-email/gatsby-config.js
new file mode 100644
index 0000000000000..d0a63de9a9b04
--- /dev/null
+++ b/examples/functions-sendgrid-email/gatsby-config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ flags: {
+ FUNCTIONS: true,
+ },
+ siteMetadata: {
+ title: "Sendgrid Email",
+ },
+ plugins: ["gatsby-plugin-gatsby-cloud"],
+}
diff --git a/examples/functions-sendgrid-email/package.json b/examples/functions-sendgrid-email/package.json
new file mode 100644
index 0000000000000..5fc63b845dc18
--- /dev/null
+++ b/examples/functions-sendgrid-email/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "sendgrid-email",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Sendgrid Email",
+ "author": "Joel Smith",
+ "keywords": [
+ "gatsby"
+ ],
+ "scripts": {
+ "develop": "gatsby develop",
+ "start": "gatsby develop",
+ "build": "gatsby build",
+ "serve": "gatsby serve",
+ "clean": "gatsby clean"
+ },
+ "dependencies": {
+ "@sendgrid/mail": "^7.4.2",
+ "gatsby": "^3.4.1",
+ "gatsby-plugin-gatsby-cloud": "^2.3.0",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1"
+ }
+}
diff --git a/examples/functions-sendgrid-email/src/api/sendgrid.js b/examples/functions-sendgrid-email/src/api/sendgrid.js
new file mode 100644
index 0000000000000..4e625858a5c25
--- /dev/null
+++ b/examples/functions-sendgrid-email/src/api/sendgrid.js
@@ -0,0 +1,43 @@
+const sendgrid = require("@sendgrid/mail")
+//Your API Key from Sendgrid
+sendgrid.setApiKey(process.env.SENDGRID_API_KEY)
+const message = {
+ //Your authorized email from SendGrid
+ from: process.env.SENDGRID_AUTHORIZED_EMAIL,
+}
+
+const handler = (req, res) => {
+ try {
+ if (req.method !== "POST") {
+ res.json({ message: "Try a POST!" })
+ }
+
+ if (req.body) {
+ message.to = req.body.email
+ message.subject = req.body.subject
+ message.text = req.body.text
+ message.html = req.body.text
+ }
+
+ return sendgrid.send(message).then(
+ () => {
+ res.status(200).json({
+ message: "I will send email",
+ })
+ },
+ error => {
+ console.error(error)
+ if (error.response) {
+ return res.status(500).json({
+ error: error.response,
+ })
+ }
+ }
+ )
+ } catch (err) {
+ console.log(err)
+ return res.status(500).json({ message: "There was an error", error: err })
+ }
+}
+
+module.exports = handler
diff --git a/examples/functions-sendgrid-email/src/images/icon.png b/examples/functions-sendgrid-email/src/images/icon.png
new file mode 100644
index 0000000000000..38b2fb0e467e0
Binary files /dev/null and b/examples/functions-sendgrid-email/src/images/icon.png differ
diff --git a/examples/functions-sendgrid-email/src/pages/404.js b/examples/functions-sendgrid-email/src/pages/404.js
new file mode 100644
index 0000000000000..053ae0e831ee9
--- /dev/null
+++ b/examples/functions-sendgrid-email/src/pages/404.js
@@ -0,0 +1,54 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+// styles
+const pageStyles = {
+ color: "#232129",
+ padding: "96px",
+ fontFamily: "-apple-system, Roboto, sans-serif, serif",
+}
+const headingStyles = {
+ marginTop: 0,
+ marginBottom: 64,
+ maxWidth: 320,
+}
+
+const paragraphStyles = {
+ marginBottom: 48,
+}
+const codeStyles = {
+ color: "#8A6534",
+ padding: 4,
+ backgroundColor: "#FFF4DB",
+ fontSize: "1.25rem",
+ borderRadius: 4,
+}
+
+// markup
+const NotFoundPage = () => {
+ return (
+
+ Not found
+
Page not found
+
+ Sorry{" "}
+
+ 😔
+ {" "}
+ we couldn’t find what you were looking for.
+
+ {process.env.NODE_ENV === "development" ? (
+ <>
+
+ Try creating a page in src/pages/.
+
+ >
+ ) : null}
+
+ Go home.
+
+
+ )
+}
+
+export default NotFoundPage
diff --git a/examples/functions-sendgrid-email/src/pages/index.js b/examples/functions-sendgrid-email/src/pages/index.js
new file mode 100644
index 0000000000000..13b35a1b4f9be
--- /dev/null
+++ b/examples/functions-sendgrid-email/src/pages/index.js
@@ -0,0 +1,36 @@
+import * as React from "react"
+
+export default function SendGridUI() {
+ return (
+
+ )
+}
diff --git a/examples/functions-twilio-text/LICENSE b/examples/functions-twilio-text/LICENSE
new file mode 100644
index 0000000000000..23eb9b6463403
--- /dev/null
+++ b/examples/functions-twilio-text/LICENSE
@@ -0,0 +1,14 @@
+The BSD Zero Clause License (0BSD)
+
+Copyright (c) 2020 Gatsby Inc.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
\ No newline at end of file
diff --git a/examples/functions-twilio-text/README.md b/examples/functions-twilio-text/README.md
new file mode 100644
index 0000000000000..1f6407f8b5bb6
--- /dev/null
+++ b/examples/functions-twilio-text/README.md
@@ -0,0 +1,59 @@
+
+
+## 🚀 Quick start
+
+1. **Get Twilio Credentials.**
+
+ Create an account on [Twilio](https://www.twilio.com/). We'll use WhatsApp for this demo as Twilio has a free test sandbox for WhatsApp (where with SMS you must first purchase a phone number). Head to the [WhatsApp getting started page](https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn?frameUrl=%2Fconsole%2Fsms%2Fwhatsapp%2Flearn%3Fx-target-region%3Dus1) to enable your account to use the sandbox.
+
+ There are **3** environment variable you'll need to add your project to properly run the starter:
+
+ - `TWILIO_ACCOUNT_SID`: Get your Twilio Account SID in the [Twilio Console Dashboard](https://www.twilio.com/console)
+ - `TWILIO_AUTH_TOKEN`: Get your Twilio Auth Token in the [Twilio Console Dashboard](https://www.twilio.com/console)
+ - `TWILIO_NUMBER`: Use the Twilio WhatsApp sandbox number `whatsapp:+14155238886`
+
+ You'll want to add these as environment variables when deploying to Gatsby Cloud. Don't forget to add them to the Preview variables if you plan to add a CMS preview integration.
+
+2. **Start developing.**
+
+ To get started, run `yarn` to add all necessary packages.
+
+ When developing locally, you'll want to include the ENV variables in your `.env.development`. Read more about how Gatsby handles `.env` files and environment variables in the [Gatbsy Docs](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/)
+
+ ```shell
+ cd twilio-text
+ yarn
+ yarn run develop
+ ```
+
+3. **Open the code and start customizing!**
+
+ Your site is now running at http://localhost:8000! You can use the UI on the index page to test the functions or directly access them at http://localhost:8000/api/twilio
+
+ When sending messages to WhatsApp numbers, the format you use is `whatsapp:` + the number e.g. `whatsapp:+14004004000`
+
+ Edit `src/pages/index.js` to see your site update in real-time!
+
+4. **Deploy**
+
+You can deploy this example on Gatsby Cloud by copying the example into a new repo and [connecting that to Gatsby Cloud](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/deploying-to-gatsby-cloud/#set-up-an-existing-gatsby-site).
+
+
diff --git a/examples/functions-twilio-text/gatsby-config.js b/examples/functions-twilio-text/gatsby-config.js
new file mode 100644
index 0000000000000..5d775921c892a
--- /dev/null
+++ b/examples/functions-twilio-text/gatsby-config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ flags: {
+ FUNCTIONS: true,
+ },
+ siteMetadata: {
+ title: "Twilio Text",
+ },
+ plugins: ["gatsby-plugin-gatsby-cloud"],
+}
diff --git a/examples/functions-twilio-text/package.json b/examples/functions-twilio-text/package.json
new file mode 100644
index 0000000000000..66cf4990a7a38
--- /dev/null
+++ b/examples/functions-twilio-text/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "twilio-text",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Twilio Text",
+ "author": "Polina Vorozheykina",
+ "keywords": [
+ "gatsby"
+ ],
+ "scripts": {
+ "develop": "gatsby develop",
+ "start": "gatsby develop",
+ "build": "gatsby build",
+ "serve": "gatsby serve",
+ "clean": "gatsby clean"
+ },
+ "dependencies": {
+ "gatsby": "^3.4.1",
+ "gatsby-plugin-functions": "^0.1.0-7",
+ "gatsby-plugin-gatsby-cloud": "^2.3.0",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1",
+ "twilio": "^3.61.0"
+ }
+}
diff --git a/examples/functions-twilio-text/src/api/twilio.js b/examples/functions-twilio-text/src/api/twilio.js
new file mode 100644
index 0000000000000..cf388a33326c9
--- /dev/null
+++ b/examples/functions-twilio-text/src/api/twilio.js
@@ -0,0 +1,41 @@
+const message = {
+ //Your authorized phone number from Twilio
+ from: process.env.TWILIO_NUMBER,
+}
+const accountSid = process.env.TWILIO_ACCOUNT_SID
+const authToken = process.env.TWILIO_AUTH_TOKEN
+const twilio = require("twilio")(accountSid, authToken)
+
+const handler = async (req, res) => {
+ try {
+ if (req.method !== "POST") {
+ res.json({ message: "Try a POST!" })
+ }
+
+ if (req.body) {
+ message.body = req.body.text
+ message.to = req.body.to
+ }
+
+ return twilio.messages.create(message).then(
+ () => {
+ return res.status(200).json({
+ message: "I will send test message",
+ })
+ },
+ error => {
+ console.error(error)
+ if (error.response) {
+ return res.status(500).json({
+ error: error.response,
+ })
+ }
+ }
+ )
+ } catch (err) {
+ console.log(err)
+ return res.status(500).json({ message: "There was an error", error: err })
+ }
+}
+
+module.exports = handler
diff --git a/examples/functions-twilio-text/src/images/icon.png b/examples/functions-twilio-text/src/images/icon.png
new file mode 100644
index 0000000000000..38b2fb0e467e0
Binary files /dev/null and b/examples/functions-twilio-text/src/images/icon.png differ
diff --git a/examples/functions-twilio-text/src/pages/404.js b/examples/functions-twilio-text/src/pages/404.js
new file mode 100644
index 0000000000000..053ae0e831ee9
--- /dev/null
+++ b/examples/functions-twilio-text/src/pages/404.js
@@ -0,0 +1,54 @@
+import * as React from "react"
+import { Link } from "gatsby"
+
+// styles
+const pageStyles = {
+ color: "#232129",
+ padding: "96px",
+ fontFamily: "-apple-system, Roboto, sans-serif, serif",
+}
+const headingStyles = {
+ marginTop: 0,
+ marginBottom: 64,
+ maxWidth: 320,
+}
+
+const paragraphStyles = {
+ marginBottom: 48,
+}
+const codeStyles = {
+ color: "#8A6534",
+ padding: 4,
+ backgroundColor: "#FFF4DB",
+ fontSize: "1.25rem",
+ borderRadius: 4,
+}
+
+// markup
+const NotFoundPage = () => {
+ return (
+
+ Not found
+
Page not found
+
+ Sorry{" "}
+
+ 😔
+ {" "}
+ we couldn’t find what you were looking for.
+
+ {process.env.NODE_ENV === "development" ? (
+ <>
+
+ Try creating a page in src/pages/.
+
+ >
+ ) : null}
+
+ Go home.
+