Overriding Gatsby-Link for Mixed Anchor Navigation

Date:

Topics: Gatsby, React

When I started to build this site, I knew that I wanted to house a lot of the information on the home page for better search engine optimization. I also knew that I would need some additional real estate for a blog and whatever else I might dream up. One of my favorite UI patterns for longform content is blocked sections with anchor scrolling. This pattern typically lends itself to single-page sites, but we're gonna cheat the system here today.

Ok, so since we're using Gatsby, we've got to use it's gatsby-link package, which plays nice with all of the routing under the hood. Cool.

Let's think about how this nav is going to work from a logistical standpoint:

  • My main 4-5 nav items relate to blocked sections at route /
  • One or two nav items will lead users to another route, let's say /blog
  • While I'm on a non-root path, I need my 4-5 main nav items to know that I need to head back to / and then magically find the #block it refers to
  • I don't want this to look or act stupid

The non-anchor link can just use the typical functionality of gatsby-link - so we just need to figure out how the 4-5 main links are going to situationally process what to do.

I'm thinking an onClick prop on my <Link /> component will probably do the trick here. Let's take a look at what it might look like:

export default class Header extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }
  
  _handleLinkClick = (e, target) => {
    
    // NODE-SAFE CODE
    // Gatsby uses Node to generate our pages. 
    // Node doesn't know what a window is. 
    // Be sure to wrap any of your browser interactions
    // in some sort of node-safe if statement like this:
    
    if (typeof window !== undefined) {
      
      // First, are we on the home page?
      // If so, let's scroll to the desired block,
      // which was passed in as an onClick prop on our Link.
      // An event was also passed, we'll preventDefault()
      
      if (window.location.pathname === '/') {
        e.preventDefault()
        scrollToElement(target, {
          offset: -95,
          duration: 1000,
        })
      }
    }
  }

  render() {
    return (
      <HeaderWrapper>
        <Container>
          <div className="nav">
            <Link
              onClick={e => this._handleLinkClick(e, '#about')}
              to={'/#about'}
            >
              About
            </Link>
            <Link
              onClick={e => this._handleLinkClick(e, '#experience')}
              to={'/#experience'}
            >
              Experience
            </Link>
            <Link
              onClick={e => this._handleLinkClick(e, '#work')}
              to={'/#work'}
            >
              Work
            </Link>
            <Link
              onClick={e => this._handleLinkClick(e, '#clients')}
              to={'/#clients'}
            >
              Clients
            </Link>
            <Link
              onClick={e => this._handleLinkClick(e, '#testimonials')}
              to={'/#testimonials'}
            >
              Testimonials
            </Link>
            <Link className="divider" to={'/contact'}>
              Contact
            </Link>

            <Link to={'/blog'}>Blog</Link>
          </div>
        </Container>
      </HeaderWrapper>
    )
  }
}

Okay, so this should work if we're on the home page. But what if we're not? I still want some super smooth scroll action if I've got to hop from one route to another.

After 83 seconds of research, you'll find the Gatsby Browser API to be the one for you. Here we can listen for each route change, and then do something if some criterion is met.

In our gatsby-browser.js file, let's look for /#block, which you'll notice our <Link /> components are pointed to in the snippet above.

const scrollToElement = require('scroll-to-element')

exports.onRouteUpdate = ({ location }) => {
  checkHash(location)
}

const checkHash = location => {
  let hash = location.hash
  if (hash) {
    scrollToElement(hash, {
      offset: -90,
      duration: 1000,
    })
  }
}

I prefer using a pre-packaged scrolling solution like scroll-to-element because it's polished & polyfilled. You could easily implement your own solution here to that end.

In any case, all we're doing here is checking to see if any hash value has been appended to the route we've just hit. If so, you know that we've returned to the home page from a non-root route. That'll get it done!