diff --git a/gatsby-config.js b/gatsby-config.js index 9952819eaf..85f6d63c79 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -85,6 +85,16 @@ if (process.env.CONTEXT === 'production') { }) } +if (process.env.ANALYZE) { + plugins.push({ + resolve: 'gatsby-plugin-webpack-bundle-analyzer', + options: { + analyzerPort: 4000, + production: process.env.NODE_ENV === 'production' + } + }) +} + module.exports = { plugins, siteMetadata: { diff --git a/gatsby-node.js b/gatsby-node.js index 2c3a68763c..d80f163885 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -105,10 +105,24 @@ exports.createPages = async ({ graphql, actions }) => { }) } +const notFoundRegexp = /^\/404/ +const trailingSlashRegexp = /\/$/ + exports.onCreatePage = ({ page, actions }) => { - if (/^\/404/.test(page.path)) { - const newPage = { ...page, context: { ...page.context, is404: true } } + let newPage = page + + if (notFoundRegexp.test(newPage.path)) { + newPage = { ...newPage, context: { ...newPage.context, is404: true } } + } + + if (page.path !== '/' && trailingSlashRegexp.test(newPage.path)) { + newPage = { + ...newPage, + path: newPage.path.replace(trailingSlashRegexp, '') + } + } + if (newPage !== page) { actions.deletePage(page) actions.createPage(newPage) } diff --git a/middleware/notFound/index.js b/middleware/notFound/index.js deleted file mode 100644 index 677c77e159..0000000000 --- a/middleware/notFound/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-env node */ - -const path = require('path') - -module.exports = (req, res) => { - res.sendFile(path.join(`${__dirname}`, '..', '..', 'public', '404.html')) -} diff --git a/package.json b/package.json index b698963263..ef96dac650 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "url": "git+https://github.com/iterative/dvc.org.git" }, "author": "", - "license": "ISC", + "license": "Apache-2.0", "bugs": { "url": "https://github.com/iterative/dvc.org/issues" }, @@ -32,12 +32,14 @@ "@reach/router": "^1.3.1", "@sentry/browser": "^5.12.1", "color": "^3.1.2", + "compression": "^1.7.4", "date-fns": "^2.8.1", "docsearch.js": "^2.6.3", "dom-scroll-into-view": "^2.0.1", "express": "^4.17.1", "gatsby": "^2.19.21", "gatsby-link": "^2.2.29", + "gatsby-plugin-webpack-bundle-analyzer": "^1.0.5", "github-markdown-css": "^3.0.1", "isomorphic-fetch": "^2.2.1", "lodash.fill": "^3.4.0", @@ -50,6 +52,7 @@ "node-cache": "^5.1.0", "perfect-scrollbar": "^1.4.0", "prismjs": "^1.19.0", + "promise-polyfill": "^8.1.3", "prop-types": "^15.7.2", "react": "^16.12.0", "react-collapse": "^5.0.1", @@ -64,6 +67,7 @@ "react-use": "^13.24.0", "rehype-react": "^4.0.1", "request": "^2.88.0", + "serve-handler": "^6.1.2", "slick-carousel": "^1.8.1", "styled-components": "^4.4.1", "styled-reset": "^4.0.8", diff --git a/server.js b/server.js index 467723dff0..341f0c1f69 100644 --- a/server.js +++ b/server.js @@ -1,19 +1,47 @@ /* eslint-env node */ const express = require('express') +const compression = require('compression') +const serveHandler = require('serve-handler') + const app = express() const apiMiddleware = require('./middleware/api') const redirectsMiddleware = require('./middleware/redirects') -const notFoundMiddleware = require('./middleware/notFound') const port = process.env.PORT || 3000 +app.use(compression()) app.use(redirectsMiddleware) app.use('/api', apiMiddleware) -app.use(express.static('public', { cacheControl: true, maxAge: 0 })) - -app.use(notFoundMiddleware) +app.use((req, res) => { + serveHandler(req, res, { + public: 'public', + cleanUrls: true, + trailingSlash: false, + directoryListing: false, + headers: [ + { + source: '**/*.@(jpg|jpeg|gif|png)', + headers: [ + { + key: 'Cache-Control', + value: 'max-age=86400' + } + ] + }, + { + source: '!**/*.@(jpg|jpeg|gif|png)', + headers: [ + { + key: 'Cache-Control', + value: 'max-age=0' + } + ] + } + ] + }) +}) app.listen(port, () => console.log(`Ready on localhost:${port}!`)) diff --git a/src/components/DocLayout/SidebarMenu/index.js b/src/components/DocLayout/SidebarMenu/index.js index bbbd346214..be2cd939cb 100644 --- a/src/components/DocLayout/SidebarMenu/index.js +++ b/src/components/DocLayout/SidebarMenu/index.js @@ -76,7 +76,7 @@ export default function SidebarMenu({ id, sidebar, currentPath, onClick }) { }) } - const node = document.getElementById(currentPath.replace(/\/$/, '')) + const node = document.getElementById(currentPath) const parent = document.getElementById(id) setIsScrollHidden(true) diff --git a/src/components/DocLayout/index.js b/src/components/DocLayout/index.js index 49b989aba5..07661b874f 100644 --- a/src/components/DocLayout/index.js +++ b/src/components/DocLayout/index.js @@ -1,13 +1,11 @@ -/* global docsearch:readonly */ - -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useState } from 'react' import PropTypes from 'prop-types' import Hamburger from '../Hamburger' import SearchForm from '../SearchForm' import MainLayout from '../MainLayout' import SidebarMenu from './SidebarMenu' -import { Container, Backdrop, SearchArea, Side, SideToggle } from './styles' +import { Container, Backdrop, Side, SideToggle } from './styles' import { structure } from '../../utils/sidebar' @@ -15,29 +13,9 @@ const SIDEBAR_MENU = 'sidebar-menu' function DocLayout({ children, ...restProps }) { const [isMenuOpen, setIsMenuOpen] = useState(false) - const [isSearchAvaible, setIsSearchAvaible] = useState(false) const toggleMenu = useCallback(() => setIsMenuOpen(!isMenuOpen), [isMenuOpen]) - useEffect(() => { - try { - docsearch - - setIsSearchAvaible(true) - - if (docsearch && isSearchAvaible) { - docsearch({ - apiKey: '755929839e113a981f481601c4f52082', - indexName: 'dvc', - inputSelector: '#doc-search', - debug: false // Set to `true` if you want to inspect the dropdown - }) - } - } catch (ReferenceError) { - // nothing there - } - }, [isSearchAvaible]) - return ( @@ -48,12 +26,7 @@ function DocLayout({ children, ...restProps }) { - {isSearchAvaible && ( - - - - )} - + { - // IE can't use forEach on array-likes - const imagesArray = Array.prototype.slice.call(images) - - if (imagesArray.length) { - let counter = imagesArray.length - - const unsubscribe = () => { - imagesArray.forEach(img => { - img.removeEventListener('load', decrement) - img.removeEventListener('error', decrement) - }) - } - - const decrement = () => { - counter -= 1 - - if (!counter) { - callback() - unsubscribe() - } - } - - imagesArray.forEach(img => { - img.addEventListener('load', decrement) - img.addEventListener('error', decrement) - }) - - setTimeout(() => { - if (counter) unsubscribe() - }, 5000) - } -} - export default class RightPanel extends React.PureComponent { state = { height: 0, @@ -79,15 +47,10 @@ export default class RightPanel extends React.PureComponent { window.removeEventListener('resize', this.updateHeadingsPosition) } - initHeadingsPosition = () => { - const images = document.querySelectorAll(`${MARKDOWN_ROOT} img`) - - if (images.length) { - imageChecker(images, this.updateHeadingsPosition) - } else { - this.updateHeadingsPosition() - } - } + initHeadingsPosition = () => + allImagesLoadedInContainer(document.querySelector(MARKDOWN_ROOT)).then( + this.updateHeadingsPosition + ) updateHeadingsPosition = () => { const coordinates = this.props.headings.reduce((result, { slug }) => { diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js index e74397bd04..2fd0c70177 100644 --- a/src/components/Layout/index.js +++ b/src/components/Layout/index.js @@ -6,22 +6,28 @@ import { GlobalStyle } from '../../styles' import MainLayout from '../MainLayout' import DocLayout from '../DocLayout' +import { allImagesLoadedInContainer } from '../../utils/images' + import './fonts/fonts.css' const useAnchorNavigation = () => { const location = useLocation() useEffect(() => { + const bodybag = document.getElementById('bodybag') + + if (!bodybag) { + return + } + if (location.hash) { const node = document.querySelector(location.hash) if (node) { - node.scrollIntoView() + allImagesLoadedInContainer(bodybag).then(() => node.scrollIntoView()) } } else { - document - .getElementById('bodybag') - .scrollTo({ top: 0, behavior: 'smooth' }) + bodybag.scrollTop = 0 } }, [location.href]) } diff --git a/src/components/SEO/index.js b/src/components/SEO/index.js index 5826a24a00..53359b8f7f 100644 --- a/src/components/SEO/index.js +++ b/src/components/SEO/index.js @@ -37,11 +37,11 @@ function SEO({ const defaultMeta = [ { - property: 'description', + name: 'description', content: metaDescription }, { - property: 'keywords', + name: 'keywords', content: metaKeywords }, { diff --git a/src/components/SearchForm/index.js b/src/components/SearchForm/index.js index 69bcb809e7..a97911c835 100644 --- a/src/components/SearchForm/index.js +++ b/src/components/SearchForm/index.js @@ -1,16 +1,55 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' +import Promise from 'promise-polyfill' +import { loadResource } from '../../utils/resources' -import { Input, Wrapper } from './styles' +import { SearchArea, Input, Wrapper } from './styles' export default function SearchForm(props) { + const [isLoaded, setLoaded] = useState(false) + + useEffect(() => { + // When mailchimp loads it adds AMD support and docsearch define new AMD + // unnamed(!) modules instead of global variable + if (window.define) { + window.define.amd = false + } + + Promise.all([ + loadResource( + 'https://cdn.jsdelivr.net/npm/docsearch.js@2.6.2/dist/cdn/docsearch.min.css' + ), + loadResource( + 'https://cdn.jsdelivr.net/npm/docsearch.js@2.6.2/dist/cdn/docsearch.min.js' + ) + ]).then(() => setLoaded(true)) + }, []) + + useEffect(() => { + if (isLoaded) { + window.docsearch && + window.docsearch({ + apiKey: '755929839e113a981f481601c4f52082', + indexName: 'dvc', + inputSelector: '#doc-search', + debug: false // Set to `true` if you want to inspect the dropdown + }) + } + }, [isLoaded]) + + if (!isLoaded) { + return null + } + return ( - - - + + + + + ) } diff --git a/src/components/SearchForm/styles.js b/src/components/SearchForm/styles.js index ba988dc67b..e660a733aa 100644 --- a/src/components/SearchForm/styles.js +++ b/src/components/SearchForm/styles.js @@ -1,5 +1,26 @@ import styled from 'styled-components' +import { media } from '../../styles' + +export const SearchArea = styled.div` + height: 60px; + display: flex; + align-items: center; + background-color: #eef4f8; + z-index: 10; + position: sticky; + top: 0; + + ${media.phablet` + position: relative; + padding: 0 20px; + `}; + + form { + height: 40px; + } +` + export const Wrapper = styled.form` width: 100%; height: 100%; diff --git a/src/html.js b/src/html.js index fb11f8fe40..b6f69dd007 100644 --- a/src/html.js +++ b/src/html.js @@ -1,5 +1,9 @@ /* eslint jsx-a11y/html-has-lang:0 */ +// polyfills +import 'promise-polyfill/src/polyfill' +import 'isomorphic-fetch' + import React from 'react' import PropTypes from 'prop-types' @@ -13,14 +17,6 @@ export default function HTML(props) { name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - -