diff --git a/docs/src/gatsby-theme-apollo-docs/components/multi-code-block.js b/docs/src/gatsby-theme-apollo-docs/components/multi-code-block.js
new file mode 100644
index 000000000..c9346ebb5
--- /dev/null
+++ b/docs/src/gatsby-theme-apollo-docs/components/multi-code-block.js
@@ -0,0 +1,127 @@
+import PropTypes from 'prop-types';
+import React, {createContext, useContext, useMemo} from 'react';
+import styled from '@emotion/styled';
+import {trackCustomEvent} from 'gatsby-plugin-google-analytics';
+
+export const GA_EVENT_CATEGORY_CODE_BLOCK = 'Code Block';
+export const MultiCodeBlockContext = createContext({});
+export const SelectedLanguageContext = createContext();
+
+const Container = styled.div({
+ position: 'relative'
+});
+
+const langLabels = {
+ js: 'JavaScript',
+ ts: 'TypeScript',
+ 'hooks-js': 'Hooks (JS)',
+ 'hooks-ts': 'Hooks (TS)'
+};
+
+function getUnifiedLang(language) {
+ switch (language) {
+ case 'js':
+ case 'jsx':
+ case 'javascript':
+ return 'js';
+ case 'ts':
+ case 'tsx':
+ case 'typescript':
+ return 'ts';
+ default:
+ return language;
+ }
+}
+
+function getLang(child) {
+ return getUnifiedLang(child.props['data-language']);
+}
+
+export function MultiCodeBlock(props) {
+ const {codeBlocks, titles} = useMemo(() => {
+ const defaultState = {
+ codeBlocks: {},
+ titles: {}
+ };
+
+ if (!Array.isArray(props.children)) {
+ return defaultState;
+ }
+
+ return props.children.reduce((acc, child, index, array) => {
+ const lang = getLang(child);
+ if (lang) {
+ return {
+ ...acc,
+ codeBlocks: {
+ ...acc.codeBlocks,
+ [lang]: child
+ }
+ };
+ }
+
+ if (child.props.className === 'gatsby-code-title') {
+ const nextNode = array[index + 1];
+ const title = child.props.children;
+ const lang = getLang(nextNode);
+ if (nextNode && title && lang) {
+ return {
+ ...acc,
+ titles: {
+ ...acc.titles,
+ [lang]: title
+ }
+ };
+ }
+ }
+
+ return acc;
+ }, defaultState);
+ }, [props.children]);
+
+ const languages = useMemo(() => Object.keys(codeBlocks), [codeBlocks]);
+ const [selectedLanguage, setSelectedLanguage] = useContext(
+ SelectedLanguageContext
+ );
+
+ if (!languages.length) {
+ return props.children;
+ }
+
+ function handleLanguageChange(language) {
+ setSelectedLanguage(language);
+ trackCustomEvent({
+ category: GA_EVENT_CATEGORY_CODE_BLOCK,
+ action: 'Change language',
+ label: language
+ });
+ }
+
+ const defaultLanguage = languages[0];
+ const renderedLanguage =
+ selectedLanguage in codeBlocks ? selectedLanguage : defaultLanguage;
+
+ return (
+
+ ({
+ lang,
+ label:
+ // try to find a label or capitalize the provided lang
+ langLabels[lang] || lang.charAt(0).toUpperCase() + lang.slice(1)
+ })),
+ onLanguageChange: handleLanguageChange
+ }}
+ >
+ {titles[renderedLanguage]}
+ {codeBlocks[renderedLanguage]}
+
+
+ );
+}
+
+MultiCodeBlock.propTypes = {
+ children: PropTypes.node.isRequired
+};
diff --git a/docs/src/gatsby-theme-apollo-docs/components/page-layout.js b/docs/src/gatsby-theme-apollo-docs/components/page-layout.js
new file mode 100644
index 000000000..7c97481ab
--- /dev/null
+++ b/docs/src/gatsby-theme-apollo-docs/components/page-layout.js
@@ -0,0 +1,312 @@
+import '../prism.less';
+import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
+import DocsetSwitcher from './docset-switcher';
+import Header from './header';
+import HeaderButton from './header-button';
+import PropTypes from 'prop-types';
+import React, {createContext, useMemo, useRef, useState} from 'react';
+import Search from './search';
+import styled from '@emotion/styled';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
+import {Button} from '@apollo/space-kit/Button';
+import {
+ FlexWrapper,
+ Layout,
+ MenuButton,
+ Sidebar,
+ SidebarNav,
+ breakpoints,
+ colors,
+ useResponsiveSidebar
+} from 'gatsby-theme-apollo-core';
+import {Helmet} from 'react-helmet';
+import {IconLayoutModule} from '@apollo/space-kit/icons/IconLayoutModule';
+import {Link, graphql, navigate, useStaticQuery} from 'gatsby';
+import {MobileLogo} from './mobile-logo';
+import {Select} from './select';
+import {SelectedLanguageContext} from './multi-code-block';
+import {getSpectrumUrl, getVersionBasePath} from '../utils';
+import {groupBy} from 'lodash';
+import {size} from 'polished';
+import {trackCustomEvent} from 'gatsby-plugin-google-analytics';
+
+const Main = styled.main({
+ flexGrow: 1
+});
+
+const ButtonWrapper = styled.div({
+ flexGrow: 1
+});
+
+const StyledButton = styled(Button)({
+ width: '100%',
+ ':not(:hover)': {
+ backgroundColor: colors.background
+ }
+});
+
+const StyledIcon = styled(IconLayoutModule)(size(16), {
+ marginLeft: 'auto'
+});
+
+const MobileNav = styled.div({
+ display: 'none',
+ [breakpoints.md]: {
+ display: 'flex',
+ alignItems: 'center',
+ marginRight: 32,
+ color: colors.text1
+ }
+});
+
+const HeaderInner = styled.span({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: 32
+});
+
+const Eyebrow = styled.div({
+ flexShrink: 0,
+ padding: '8px 56px',
+ backgroundColor: colors.background,
+ color: colors.primary,
+ fontSize: 14,
+ position: 'sticky',
+ top: 0,
+ a: {
+ color: 'inherit',
+ fontWeight: 600
+ },
+ [breakpoints.md]: {
+ padding: '8px 24px'
+ }
+});
+
+function getVersionLabel(version) {
+ return `v${version}`;
+}
+
+const GA_EVENT_CATEGORY_SIDEBAR = 'Sidebar';
+
+function handleToggleAll(expanded) {
+ trackCustomEvent({
+ category: GA_EVENT_CATEGORY_SIDEBAR,
+ action: 'Toggle all',
+ label: expanded ? 'expand' : 'collapse'
+ });
+}
+
+function handleToggleCategory(label, expanded) {
+ trackCustomEvent({
+ category: GA_EVENT_CATEGORY_SIDEBAR,
+ action: 'Toggle category',
+ label,
+ value: Number(expanded)
+ });
+}
+
+export const NavItemsContext = createContext();
+
+export default function PageLayout(props) {
+ const data = useStaticQuery(
+ graphql`
+ {
+ site {
+ siteMetadata {
+ title
+ siteName
+ }
+ }
+ }
+ `
+ );
+
+ const {
+ sidebarRef,
+ openSidebar,
+ sidebarOpen,
+ handleWrapperClick,
+ handleSidebarNavLinkClick
+ } = useResponsiveSidebar();
+
+ const buttonRef = useRef(null);
+ const [menuOpen, setMenuOpen] = useState(false);
+ const selectedLanguageState = useLocalStorage('docs-lang');
+
+ function openMenu() {
+ setMenuOpen(true);
+ }
+
+ function closeMenu() {
+ setMenuOpen(false);
+ }
+
+ const {pathname} = props.location;
+ const {siteName, title} = data.site.siteMetadata;
+ const {
+ subtitle,
+ sidebarContents,
+ versions,
+ versionDifference,
+ versionBasePath,
+ defaultVersion
+ } = props.pageContext;
+ const {
+ spectrumHandle,
+ twitterHandle,
+ youtubeUrl,
+ navConfig = {},
+ footerNavConfig,
+ logoLink,
+ algoliaApiKey,
+ algoliaIndexName,
+ menuTitle
+ } = props.pluginOptions;
+
+ const {navItems, navCategories} = useMemo(() => {
+ const navItems = Object.entries(navConfig).map(([title, navItem]) => ({
+ ...navItem,
+ title
+ }));
+ return {
+ navItems,
+ navCategories: Object.entries(groupBy(navItems, 'category'))
+ };
+ }, [navConfig]);
+
+ const hasNavItems = navItems.length > 0;
+ const sidebarTitle = (
+ {subtitle || siteName}
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ {hasNavItems ? (
+
+
+ {sidebarTitle}
+
+
+
+ ) : (
+ sidebarTitle
+ )}
+ {versions && versions.length > 0 && (
+
+ {sidebarContents && (
+
+ )}
+
+
+
+ You're viewing documentation for a{' '}
+ {versionDifference > 0
+ ? 'version of this software that is in development'
+ : 'previous version of this software'}
+ . Switch to the latest stable version
+
+ )
+ }
+ >
+
+
+
+
+ {algoliaApiKey && algoliaIndexName && (
+
+ )}
+
+
+
+
+ {props.children}
+
+
+
+
+ {hasNavItems && (
+
+ )}
+
+ );
+}
+
+PageLayout.propTypes = {
+ children: PropTypes.node.isRequired,
+ location: PropTypes.object.isRequired,
+ pageContext: PropTypes.object.isRequired,
+ pluginOptions: PropTypes.object.isRequired
+};
diff --git a/docs/src/gatsby-theme-apollo-docs/components/select.js b/docs/src/gatsby-theme-apollo-docs/components/select.js
new file mode 100644
index 000000000..f2bde0f84
--- /dev/null
+++ b/docs/src/gatsby-theme-apollo-docs/components/select.js
@@ -0,0 +1,133 @@
+import PropTypes from 'prop-types';
+import React, {useMemo, useRef, useState} from 'react';
+import styled from '@emotion/styled';
+import useClickAway from 'react-use/lib/useClickAway';
+import {Button} from '@apollo/space-kit/Button';
+import {IconArrowDown} from '@apollo/space-kit/icons/IconArrowDown';
+import {colors} from 'gatsby-theme-apollo-core';
+import {size} from 'polished';
+
+const Wrapper = styled.div({
+ position: 'relative'
+});
+
+const StyledIcon = styled(IconArrowDown)(size('1em'), {
+ marginLeft: 12
+});
+
+const Menu = styled.div({
+ minWidth: '100%',
+ padding: 8,
+ borderRadius: 4,
+ boxShadow: [
+ '0 3px 4px 0 rgba(18, 21, 26, 0.04)',
+ '0 4px 8px 0 rgba(18, 21, 26, 0.08)',
+ '0 0 0 1px rgba(18, 21, 26, 0.08)'
+ ].toString(),
+ backgroundColor: 'white',
+ position: 'absolute',
+ left: 0,
+ top: '100%',
+ zIndex: 1
+});
+
+const MenuItem = styled.button({
+ width: '100%',
+ padding: '1px 12px',
+ fontSize: 13,
+ lineHeight: 2,
+ textAlign: 'left',
+ border: 'none',
+ borderRadius: 4,
+ background: 'none',
+ cursor: 'pointer',
+ outline: 'none',
+ ':hover': {
+ backgroundColor: colors.background
+ },
+ '&.selected': {
+ backgroundColor: colors.primary,
+ color: 'white'
+ }
+});
+
+const LabelWrapper = styled.div({
+ position: 'relative'
+});
+
+const Spacer = styled.div({
+ visibility: 'hidden'
+});
+
+const Label = styled.div({
+ position: 'absolute',
+ top: 0,
+ left: 0
+});
+
+export function Select({className, style, options, value, onChange, ...props}) {
+ const wrapperRef = useRef(null);
+ const [open, setOpen] = useState(false);
+
+ const optionKeys = useMemo(() => Object.keys(options), [options]);
+ const labelHeight = useMemo(() => {
+ switch (props.size) {
+ case 'small':
+ return 20;
+ case 'large':
+ return 27;
+ default:
+ return 22;
+ }
+ }, [props.size]);
+
+ useClickAway(wrapperRef, () => {
+ setOpen(false);
+ });
+
+ function handleClick() {
+ setOpen(prevOpen => !prevOpen);
+ }
+
+ return (
+
+
+ {open && (
+
+ )}
+
+ );
+}
+
+Select.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ size: PropTypes.string,
+ value: PropTypes.string.isRequired,
+ options: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired
+};
diff --git a/docs/src/gatsby-theme-apollo-docs/prism.less b/docs/src/gatsby-theme-apollo-docs/prism.less
new file mode 100644
index 000000000..8f82aa952
--- /dev/null
+++ b/docs/src/gatsby-theme-apollo-docs/prism.less
@@ -0,0 +1,191 @@
+/* from https://www.gatsbyjs.org/packages/gatsby-remark-prismjs/#include-css */
+
+@font-family-monospace: 'Source Code Pro', monospace;
+@gatsby-code-title-height: 50px;
+
+.gatsby-code-title {
+ display: flex;
+ align-items: center;
+ margin-bottom: -@gatsby-code-title-height;
+ height: @gatsby-code-title-height;
+ font-size: 15px;
+ font-family: @font-family-monospace;
+ color: @color-text2;
+ padding-left: 19px;
+}
+
+.gatsby-highlight-code-line {
+ background-color: @color-background2;
+ display: block;
+ margin-right: -1em;
+ margin-left: -3.5em;
+ padding-right: 1em;
+ padding-left: 3.5em;
+}
+
+/**
+ * Add back the container background-color, border-radius, padding, margin
+ * and overflow that we removed from
.
+ */
+// .gatsby-highlight {
+// background: @color-background;
+// border: 1px solid @color-divider;
+// border-radius: 4px;
+// margin: 0.5em 0 1.45em;
+// padding: 1em;
+// overflow: auto;
+// }
+
+/**
+ * Remove the default PrismJS theme background-color, border-radius, margin,
+ * padding and overflow.
+ * 1. Make the element just wide enough to fit its content.
+ * 2. Always fill the visible space in .gatsby-highlight.
+ * 3. Adjust the position of the line numbers
+ */
+.gatsby-highlight pre[class*="language-"] {
+ background-color: transparent;
+ margin: 0;
+ padding: 0;
+ overflow: initial;
+ float: left; /* 1 */
+ min-width: 100%; /* 2 */
+}
+
+/* Adjust the position of the line numbers */
+.gatsby-highlight pre[class*="language-"].line-numbers {
+ padding-left: 2.5em;
+}
+
+.gatsby-highlight pre[class*="language-"].line-numbers .line-numbers-rows {
+ border-right: none;
+}
+
+.gatsby-highlight pre[class*="language-"].line-numbers .line-numbers-rows > span:before {
+ color: @color-text4;
+}
+
+/* Generated with http://k88hudson.github.io/syntax-highlighting-theme-generator/www */
+/* http://k88hudson.github.io/react-markdocs */
+/**
+ * @author k88hudson
+ *
+ * Based on prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+/*********************************************************
+* General
+*/
+pre[class*="language-"],
+code[class*="language-"] {
+ color: @color-text1;
+ font-size: 15px;
+ text-shadow: none;
+ font-family: @font-family-monospace;
+ direction: ltr;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ line-height: 1.5;
+ -moz-tab-size: 4;
+ -o-tab-size: 4;
+ tab-size: 4;
+ -webkit-hyphens: none;
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ hyphens: none;
+}
+@media print {
+ pre[class*="language-"],
+ code[class*="language-"] {
+ text-shadow: none;
+ }
+}
+pre[class*="language-"] {
+ padding: 1em;
+ margin: .5em 0;
+ overflow: auto;
+ background: @color-background;
+}
+:not(pre) > code[class*="language-"] {
+ padding: .1em .3em;
+ border-radius: .3em;
+ font-size: 0.9em;
+ color: @color-secondary;
+ background: @color-background2;
+}
+/*********************************************************
+* Tokens
+*/
+.namespace {
+ opacity: .7;
+}
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+ color: @color-text3;
+}
+.token.punctuation {
+ color: @color-text2;
+}
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+ color: @color-secondary;
+}
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+ color: @color-tertiary;
+}
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+ color: inherit;
+ background: transparent;
+}
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+ color: @color-primary;
+}
+.token.class-name,
+.token.function {
+ color: @color-secondary;
+}
+.token.regex,
+.token.important,
+.token.variable {
+ color: @color-warning;
+}
+.token.important,
+.token.bold {
+ font-weight: bold;
+}
+.token.italic {
+ font-style: italic;
+}
+.token.entity {
+ cursor: help;
+}
+/*********************************************************
+* Line highlighting
+*/
+pre[data-line] {
+ position: relative;
+}
+pre[class*="language-"] > code[class*="language-"] {
+ position: relative;
+}