Skip to content

Commit

Permalink
feat(gatsby): Add support for relative links (#24054)
Browse files Browse the repository at this point in the history
* Add support for relative links

* Review fixes

* Changes from review + handle invalid links better

* Changes from review + handle invalid links better

* Ensure path is tarnslated in more places

* Simplify link checks by explicitly checking for http(s)

* Fix missing rewrite

* Add tests and clear up edge cases

* Use explicit protocol list for detecting external links (sorry gopher)

* Don't double-prefix absolutified paths

* Add e2e tests

* Fix tests

* Fix calls to deprecated methods

* Add docs for relative links

* Typo

* Clarify
  • Loading branch information
ascorbic committed Jun 2, 2020
1 parent 19d6cfc commit e2c6cf2
Show file tree
Hide file tree
Showing 22 changed files with 511 additions and 70 deletions.
4 changes: 4 additions & 0 deletions docs/docs/gatsby-link.md
Expand Up @@ -348,6 +348,10 @@ const Link = ({ children, to, activeClassName, partiallyActive, ...other }) => {
export default Link
```

### Relative links

The `<Link />` component follows [the behavior of @reach/router](https://reach.tech/router/nesting) by ignoring trailing slashes and treating each page as if it were a directory when resolving relative links. For example if you are on either `/blog/my-great-page` or `/blog/my-great-page/` (note the trailing slash), a link to `../second-page` will take you to `/blog/second-page`.

### File Downloads

You can similarly check for file downloads:
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/linking-between-pages.md
Expand Up @@ -31,6 +31,10 @@ The above code will add a link to the contact page, automatically rendered in HT

> **Note:** the value `"/"` for the `to` property will take users to the home page.
## Using relative links in the `<Link />` component

Relative links are ones where the `to` property doesn't start with a `/`. These behave slightly differently from relative links in `<a>` tags, and instead follow [the behavior of @reach/router](https://reach.tech/router/nesting). This avoids confusion with trailing slashes by ignoring them entirely and treating every page as if it were a directory. For example, if you are on either `/blog/my-great-page` or `/blog/my-great-page/` (note the trailing slash), a link to `../second-page` will take you to `/blog/second-page`. Similarly, if you are on `/blog` or `/blog/` a link to `hello-world` will take you to `/blog/hello-world`.

## Using `<a>` for external links

If you are linking to pages not handled by your Gatsby site (such as on a different domain), you should use the native HTML `<a>` tag instead of Gatsby Link.
Expand Down
Expand Up @@ -33,6 +33,42 @@ describe(`navigation`, () => {
cy.location(`pathname`).should(`equal`, `/`)
})

describe(`relative links`, () => {
it(`can navigate to a subdirectory`, () => {
cy.getTestElement(`subdir-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory/page-1`)
})

it(`can navigate to a sibling page`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-2-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory/page-2`)
})

it(`can navigate to a parent page`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-parent-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory`)
})

it(`can navigate to a sibling page programatically`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-2-button-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory/page-2`)
})
})

describe(`non-existent route`, () => {
beforeEach(() => {
cy.getTestElement(`broken-link`).click().waitForRouteChange()
Expand Down
3 changes: 3 additions & 0 deletions e2e-tests/development-runtime/src/pages/index.js
Expand Up @@ -26,6 +26,9 @@ const IndexPage = ({ data }) => (
<Link to="/__non_existent_page__/" data-testid="broken-link">
Go to a broken link
</Link>
<Link to="subdirectory/page-1" data-testid="subdir-link">
Go to subdirectory
</Link>
<h2>Blog posts</h2>
<ul>
{data.posts.edges.map(({ node }) => (
Expand Down
23 changes: 23 additions & 0 deletions e2e-tests/development-runtime/src/pages/subdirectory/index.js
@@ -0,0 +1,23 @@
import * as React from "react"
import { Link, navigate } from "gatsby"

import Layout from "../../components/layout"

const IndexPage = () => (
<Layout>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link data-testid="page-2-link" to="./page-2/">
Go to page 2
</Link>
<button
data-testid="page-2-button-link"
onClick={() => navigate(`./page-2/`)}
>
Go to page 2 with navigate()
</button>
</Layout>
)

export default IndexPage
26 changes: 26 additions & 0 deletions e2e-tests/development-runtime/src/pages/subdirectory/page-1.js
@@ -0,0 +1,26 @@
import * as React from "react"
import { Link, navigate } from "gatsby"

import Layout from "../../components/layout"

const IndexPage = () => (
<Layout>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link data-testid="page-2-link" to="../page-2/">
Go to page 2
</Link>
<Link data-testid="page-parent-link" to="..">
Go up
</Link>
<button
data-testid="page-2-button-link"
onClick={() => navigate(`../page-2/`)}
>
Go to page 2 with navigate()
</button>
</Layout>
)

export default IndexPage
16 changes: 16 additions & 0 deletions e2e-tests/development-runtime/src/pages/subdirectory/page-2.js
@@ -0,0 +1,16 @@
import * as React from "react"
import { Link } from "gatsby"

import Layout from "../../components/layout"

const SecondPage = () => (
<Layout>
<h1>Hi from the second page</h1>
<p>Welcome to page 2</p>
<Link data-testid="index-link" to="../page-1">
Go back to page 1
</Link>
</Layout>
)

export default SecondPage
36 changes: 36 additions & 0 deletions e2e-tests/path-prefix/cypress/integration/navigate.js
Expand Up @@ -24,6 +24,42 @@ describe(`navigate`, () => {
.should(`eq`, withTrailingSlash(pathPrefix))
})

describe(`relative links`, () => {
it(`can navigate to a subdirectory`, () => {
cy.getTestElement(`subdir-link`)
.click()
.location(`pathname`)
.should(`eq`, `${pathPrefix}/subdirectory/page-1`)
})

it(`can navigate to a sibling page`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-2-link`)
.click()
.location(`pathname`)
.should(`eq`, `${pathPrefix}/subdirectory/page-2`)
})

it(`can navigate to a parent page`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-parent-link`)
.click()
.location(`pathname`)
.should(`eq`, `${pathPrefix}/subdirectory`)
})

it(`can navigate to a sibling page programatically`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-2-button-link`)
.click()
.location(`pathname`)
.should(`eq`, `${pathPrefix}/subdirectory/page-2`)
})
})

it(`can navigate to 404`, () => {
cy.getTestElement(`404-link`).click().waitForRouteChange()

Expand Down
3 changes: 3 additions & 0 deletions e2e-tests/path-prefix/src/pages/index.js
Expand Up @@ -23,6 +23,9 @@ const IndexPage = () => (
<Link data-testid="404-link" to="/not-existing-page">
Go to not existing page
</Link>
<Link data-testid="subdir-link" to="subdirectory/page-1">
Go to subdirectory
</Link>
</Layout>
)

Expand Down
23 changes: 23 additions & 0 deletions e2e-tests/path-prefix/src/pages/subdirectory/index.js
@@ -0,0 +1,23 @@
import * as React from "react"
import { Link, navigate } from "gatsby"

import Layout from "../../components/layout"

const IndexPage = () => (
<Layout>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link data-testid="page-2-link" to="./page-2/">
Go to page 2
</Link>
<button
data-testid="page-2-button-link"
onClick={() => navigate(`./page-2/`)}
>
Go to page 2 with navigate()
</button>
</Layout>
)

export default IndexPage
26 changes: 26 additions & 0 deletions e2e-tests/path-prefix/src/pages/subdirectory/page-1.js
@@ -0,0 +1,26 @@
import * as React from "react"
import { Link, navigate } from "gatsby"

import Layout from "../../components/layout"

const IndexPage = () => (
<Layout>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link data-testid="page-2-link" to="../page-2/">
Go to page 2
</Link>
<Link data-testid="page-parent-link" to="..">
Go up
</Link>
<button
data-testid="page-2-button-link"
onClick={() => navigate(`../page-2/`)}
>
Go to page 2 with navigate()
</button>
</Layout>
)

export default IndexPage
16 changes: 16 additions & 0 deletions e2e-tests/path-prefix/src/pages/subdirectory/page-2.js
@@ -0,0 +1,16 @@
import * as React from "react"
import { Link } from "gatsby"

import Layout from "../../components/layout"

const SecondPage = () => (
<Layout>
<h1>Hi from the second page</h1>
<p>Welcome to page 2</p>
<Link data-testid="index-link" to="../page-1">
Go back to page 1
</Link>
</Layout>
)

export default SecondPage
38 changes: 38 additions & 0 deletions e2e-tests/production-runtime/cypress/integration/1-production.js
Expand Up @@ -43,6 +43,44 @@ describe(`Production build tests`, () => {
.should(`equal`, `/page-2/`)
})

describe(`relative links`, () => {
it(`should navigate to a subdirectory`, () => {
cy.visit(`/`)
.waitForRouteChange()
.getTestElement(`subdir-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory/page-1`)
})

it(`can navigate to a sibling page`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-2-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory/page-2`)
})

it(`can navigate to a parent page`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-parent-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory`)
})

it(`can navigate to a sibling page programatically`, () => {
cy.visit(`/subdirectory/page-1`)
.waitForRouteChange()
.getTestElement(`page-2-button-link`)
.click()
.location(`pathname`)
.should(`eq`, `/subdirectory/page-2`)
})
})

it(`should show 404 page when clicking a link to a non-existent page route`, () => {
cy.visit(`/`).waitForRouteChange()

Expand Down
5 changes: 5 additions & 0 deletions e2e-tests/production-runtime/src/pages/index.js
Expand Up @@ -61,6 +61,11 @@ const IndexPage = ({ pageContext }) => (
Go to page with unicode path
</Link>
</li>
<li>
<Link to="subdirectory/page-1" data-testid="subdir-link">
Go to subdirectory
</Link>
</li>
</ul>
</Layout>
)
Expand Down
23 changes: 23 additions & 0 deletions e2e-tests/production-runtime/src/pages/subdirectory/index.js
@@ -0,0 +1,23 @@
import * as React from "react"
import { Link, navigate } from "gatsby"

import Layout from "../../components/layout"

const IndexPage = () => (
<Layout>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link data-testid="page-2-link" to="./page-2/">
Go to page 2
</Link>
<button
data-testid="page-2-button-link"
onClick={() => navigate(`./page-2/`)}
>
Go to page 2 with navigate()
</button>
</Layout>
)

export default IndexPage
26 changes: 26 additions & 0 deletions e2e-tests/production-runtime/src/pages/subdirectory/page-1.js
@@ -0,0 +1,26 @@
import * as React from "react"
import { Link, navigate } from "gatsby"

import Layout from "../../components/layout"

const IndexPage = () => (
<Layout>
<h1>Hi people</h1>
<p>Welcome to your new Gatsby site.</p>
<p>Now go build something great.</p>
<Link data-testid="page-2-link" to="../page-2/">
Go to page 2
</Link>
<Link data-testid="page-parent-link" to="..">
Go up
</Link>
<button
data-testid="page-2-button-link"
onClick={() => navigate(`../page-2/`)}
>
Go to page 2 with navigate()
</button>
</Layout>
)

export default IndexPage
16 changes: 16 additions & 0 deletions e2e-tests/production-runtime/src/pages/subdirectory/page-2.js
@@ -0,0 +1,16 @@
import * as React from "react"
import { Link } from "gatsby"

import Layout from "../../components/layout"

const SecondPage = () => (
<Layout>
<h1>Hi from the second page</h1>
<p>Welcome to page 2</p>
<Link data-testid="index-link" to="../page-1">
Go back to page 1
</Link>
</Layout>
)

export default SecondPage

0 comments on commit e2c6cf2

Please sign in to comment.