-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #29 from skirtles-code/routing-faqs
FAQ entries for common router problems
- Loading branch information
Showing
4 changed files
with
321 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
# Why is there a `#` before my route path? | ||
|
||
Vue Router will insert a `#` before the route path if you're using `createWebHashHistory()`: | ||
|
||
```js | ||
import { createRouter, createWebHashHistory } from 'vue-router' | ||
|
||
const router = createRouter({ | ||
history: createWebHashHistory(), | ||
routes: [ | ||
{ | ||
path: '/home', | ||
// ... | ||
} | ||
] | ||
}) | ||
``` | ||
|
||
If you're still using Vue 2, with Vue Router 3, then the equivalent is `mode: 'hash'`, which is the default. | ||
|
||
The example above will lead to a path like: | ||
|
||
``` | ||
http://example.com/#/home | ||
``` | ||
|
||
To avoid the hash you would use `createWebHistory()` instead, or `mode: 'history'` for old versions of Vue Router: | ||
|
||
::: code-group | ||
|
||
```js [Vue 3 / Vue Router 4] | ||
import { createRouter, createWebHistory } from 'vue-router' | ||
|
||
const router = createRouter({ | ||
history: createWebHistory(), | ||
routes: [ | ||
{ | ||
path: '/home', | ||
// ... | ||
} | ||
] | ||
}) | ||
``` | ||
|
||
```js [Vue 2 / Vue Router 3] | ||
import VueRouter from 'vue-router' | ||
|
||
const router = new VueRouter({ | ||
mode: 'history', | ||
routes: [ | ||
{ | ||
path: '/home', | ||
// ... | ||
} | ||
] | ||
}) | ||
``` | ||
|
||
::: | ||
|
||
The URL will now be: | ||
|
||
``` | ||
http://example.com/home | ||
``` | ||
|
||
The official documentation for this is at: | ||
|
||
- Vue 3 / Vue Router 4 - https://router.vuejs.org/guide/essentials/history-mode.html | ||
- Vue 2 / Vue Router 3 - https://v3.router.vuejs.org/guide/essentials/history-mode.html | ||
|
||
There are various pros and cons for the two different history modes. It is important to choose carefully between them. The use of a hash for routing is not unique to Vue Router, it is a common technique used by various client-side routers. The rest of this page goes into detail about the history of client-side routing and why the hash is used. | ||
|
||
## A brief history of client-side routing | ||
|
||
Some aspects of client-side routing can seem bizarre. It can help to understand how browsers behaved historically, leading to where we are now. This section isn't specifically about Vue Router, but it should help you to understand what Vue Router does and why. | ||
|
||
A long time ago, before JavaScript was a thing, browsers implemented a feature that allowed a page to scroll down to a specific section of the page. It used a `#` symbol in the URL. | ||
|
||
For example, if you had the URL `http://example.com/index.html#main` in your address bar, the browser would send an HTTP request to the server for the page `http://example.com/index.html`. The `#main` part, known as the *hash*, wouldn't be sent to the server. Once the HTML was loaded, the browser would search the page for an element `<a name="main"></a>` and scroll down to that. | ||
|
||
Later versions of HTML extended support to any element with an `id` attribute. So `#main` can be used to scroll to an element with `id="main"`. The usage with `<a name="main">"` is now deprecated, but using a `#` and `id` to jump to a section on a page is still a commonly used feature of HTML. | ||
|
||
When linking to a section on the same page, you can use a link like this: | ||
|
||
```html | ||
<a href="#main">Jump to main</a> | ||
``` | ||
|
||
Clicking this link will jump to the element with that `id`. The URL will be updated to include `#main` on the end, replacing any existing hash. But changing the hash won't send a new request to the server, as requests to the server don't take the hash into account. | ||
|
||
Changing the hash will also add an entry to the browser's navigation history. This allows the user to navigate back and forth between sections using the Back and Forward buttons in their browser. | ||
|
||
The hash can also be changed via JavaScript. For example: | ||
|
||
```js | ||
window.location.hash = '#main' | ||
``` | ||
|
||
Setting the hash in JavaScript behaves much like clicking on `<a href="#main">`. | ||
|
||
Other parts of the URL can also be changed via JavaScript. For example, you could change the search query using something like this: | ||
|
||
```js | ||
window.location.search = '?id=1' | ||
``` | ||
|
||
If we were previously at `http://example.com/index.html`, this will change the URL to `http://example.com/index.html?id=1`. Importantly, **this will cause the browser to load a new page from the server**. | ||
|
||
The rise of single-page applications (SPAs) began, more or less, with the introduction of AJAX. While SPAs brought many benefits, they also broke one of the key features of the web: the URL for an SPA was no longer tied to the content being shown to the user. URLs could no longer be shared and the Back/Forward buttons became meaningless. | ||
|
||
This led to a need for client-side routing. | ||
|
||
At the time, the only way to update the URL without triggering a request to the server was to change the hash. The browser would try to jump to an element with that `id`, but preventing that scrolling was a solvable problem. Using the hash became the standard way of implementing client-side routing. | ||
|
||
Hash-based routing leads to URLs like this: | ||
|
||
``` | ||
http://example.com/#/products/search | ||
``` | ||
|
||
The page `http://example.com/` would be loaded from the server, while the hash of `#/products/search` would be interpreted by the router. The exact format used for the hash varies, but using a slash-separated path like this is a relatively common approach. | ||
|
||
For a while, this was the only way to implement client-side routing. The server is completely cut from the process and only ever sees the start of the URL, without the hash. This has its advantages, as the server doesn't need to know anything about the routing. | ||
|
||
But using the hash has its problems: | ||
|
||
1. There's no way to implement SSR (server-side rendering) on the server, as it has no idea what route is being accessed. | ||
2. Search engines and other crawlers struggled to work with client-side routing, both because pages needed JavaScript and because the crawlers needed to understand the significance of the hashes when indexing the pages. | ||
3. Using the hash for routing meant it couldn't be used for its original purpose of jumping to content within a page. | ||
4. The `referer` header also doesn't include the hash, greatly limiting its usefulness. | ||
|
||
Google struggled so much with hash-based routing that it introduced a special convention to allow pages to be indexed correctly. That convention is no longer used, but you may still encounter routes that begin with `#!`, which was part of the convention. | ||
|
||
To try to remove the reliance on hashes for routing, browsers introduced the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API). This allows JavaScript to interact with the navigation history for the current page. That includes the ability to update the URL in the address bar without triggering a request to the server. | ||
|
||
This is generally a better approach, as we get back to traditional URLs that represent whatever page or resource is shown in the browser. The route might look something like this, without the need for a `#`: | ||
|
||
``` | ||
http://example.com/products/search | ||
``` | ||
|
||
The URL sent to the server includes the route path, allowing for SSR and improved SEO. | ||
|
||
But this approach also has its drawbacks. | ||
|
||
Sending the route path to the server means that the server must be configured to interpret that path correctly. Even if you aren't using SSR, you still need the server to return the correct HTML to load the rest of the page. | ||
|
||
It also complicates the use of relative paths. If the browser encounters any paths that begin with `./` or `../` then those will be interpreted as being relative to the current page URL. So if `<img src="./pic.png">` appears in the HTML, that will be loaded relative to the current route path, which might not be what you were expecting. | ||
|
||
To be clear, this is only a problem if the relative path makes it to the HTML in the browser. Bundlers such as Vite or webpack will try to rewrite relative URLs in the code and replace them with absolute URLs as part of the build. However, if you misconfigure your bundler you can still end up with broken relative paths. | ||
|
||
For more discussion of these problems, see [Why does my page fail to load when I refresh in production?](production-page-refresh). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,163 @@ | ||
# When does my page fail to load when I refresh in production? | ||
# Why does my page fail to load when I refresh in production? | ||
|
||
::: warning This answer is a stub. | ||
We are still working on writing the answers to the FAQ questions. The answer below is incomplete, but you may still find it useful. | ||
The short answer is you probably haven't configured your production webserver correctly to work with `createWebHistory()`. See <https://router.vuejs.org/guide/essentials/history-mode.html> for details and example server configurations. | ||
|
||
Another possibility is that you've configured Vite's `base` setting incorrectly. If you use a relative path, such as `base: './'`, it won't work correctly with `createWebHistory()`. | ||
|
||
If you need more help understanding the problem and how to fix it, read on! | ||
|
||
## Introduction | ||
|
||
First, let's clarify the exact symptoms of the problem we're trying to fix: | ||
|
||
1. You're using Vue Router with `createWebHistory`. If you're using an older version of Vue Router then the equivalent is `mode: 'history'`. | ||
2. You can access a route of `/` successfully. | ||
3. Navigating between routes works correctly. | ||
4. Trying to refresh the page on a route other than `/` fails. Likewise, it fails trying to access those routes directly via the browser address bar. Alternatively, you might find that a path with a single `/` works, e.g. `/home`, but deeper paths don't, like `/home/settings`. | ||
5. The problem only occurs in production. Everything works fine during development. | ||
|
||
If this doesn't sound like the same problem you're having, you might be better off trying one of the other FAQ entries: | ||
|
||
- [Why do I get a blank page in production?](./blank-page-in-production) | ||
- [How do I deploy to GitHub Pages?](./github-pages) | ||
|
||
## Vue Router history modes | ||
|
||
There are two main types of client-side routing. These are common to all client-side routing, not just Vue Router: | ||
|
||
1. The older approach is to use the URL hash for the route. For a long time this was the only way to implement client-side routing. | ||
2. In recent years, browsers have added the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API). That allows for client-side routing without using a hash. | ||
|
||
If you're new to client-side routing and would like to learn more about it, you'll find more details at: | ||
|
||
- [Why is there a `#` before my route path? - A brief history of client-side routing](./hash-before-route-path#a-brief-history-of-client-side-routing) | ||
|
||
The two types of client-side routing described above are both supported by Vue Router. | ||
|
||
To use hash-based routing, you need to use `createWebHashHistory()` when creating the router: | ||
|
||
```js | ||
import { createRouter, createWebHashHistory } from 'vue-router' | ||
|
||
const router = createRouter({ | ||
history: createWebHashHistory(), | ||
routes: [ | ||
//... | ||
] | ||
}) | ||
``` | ||
|
||
If you'd prefer to avoid the hash and update the URL path instead, you can use `createWebHistory()`: | ||
|
||
```js | ||
import { createRouter, createWebHistory } from 'vue-router' | ||
|
||
const router = createRouter({ | ||
history: createWebHistory(), | ||
routes: [ | ||
//... | ||
] | ||
}) | ||
``` | ||
|
||
The official documentation for this is at: | ||
|
||
- https://router.vuejs.org/guide/essentials/history-mode.html | ||
|
||
If you're still using Vue 2 and Vue Router 3 then the relevant settings are `mode: 'hash'` and `mode: 'history'`, with `hash` being the default. The documentation for that is at: | ||
|
||
- https://v3.router.vuejs.org/guide/essentials/history-mode.html | ||
|
||
## Webserver configuration | ||
|
||
The main advantage of hash-based routing is that the server is not involved at all. The route path doesn't even get sent to the server, so there's no need to configure anything on the server to get it to work. | ||
|
||
Using the History API to avoid the hash leads to cleaner URLs, but the advantages go way beyond aesthetics. Avoiding the hash is also better for SEO and SSR. | ||
|
||
The downside is that the request URLs being sent to your production webserver will include the route path, so the server needs to understand how to respond to that. | ||
|
||
:::info | ||
To simplify this explanation, we're going to ignore server-side rendering. | ||
|
||
We also have a separate FAQ entry for [How do I add dynamic <meta> tags to my application?](./dynamic-meta-tags), but you should make sure you understand how things work in the simpler case described here before trying to attempt more advanced configurations. | ||
::: | ||
|
||
If you're using Vue Router, it'll likely be because you haven't configured your server to work with HTML5 history mode. | ||
For a typical Vue application, you'll have a single HTML entry point called `index.html`. The server needs to return the contents of that file for all route URLs. | ||
|
||
So, for example, let's say you have three route URLs: | ||
|
||
``` | ||
http://example.com/ | ||
http://example.com/products | ||
http://example.com/products/search | ||
``` | ||
|
||
The server should return the same file for each of those three URLs. | ||
|
||
During development, the Vite (or webpack) dev server handles that for you. But in production, you need to configure the webserver yourself. | ||
|
||
After you run your build, there should be a built copy of `index.html` at `dist/index.html`. Your production webserver would need to return the contents of that file in response to all three of the requests above. | ||
|
||
In addition, you also need to take care to ensure that the webserver still returns other files correctly. There'll be various other files in `dist`, such as the built `.js` files. When the browser requests those files they need to be returned, not the contents of `index.html`. | ||
|
||
The documentation for Vue Router includes examples for configuring various popular webservers and hosting providers: | ||
|
||
- https://router.vuejs.org/guide/essentials/history-mode.html#Example-Server-Configurations | ||
|
||
You can verify whether your server is configured correctly by checking the responses you receive in the **Network** tab of your browser's developer tools. There's much more detail about how to do that here: | ||
|
||
- [Why do I get a blank page in production? - Check the network tab](./blank-page-in-production#check-the-network-tab) | ||
|
||
Some hosting providers, such as GitHub Pages, don't offer a direct way to configure the webserver to return `index.html` for all routes. That makes working with `createWebHistory()` difficult, or even impossible, depending on what exactly you need. We have a separate guide that suggests some approaches that will work with GitHub Pages, and those should also carry across to other hosting providers that don't offer this kind of configuration: | ||
|
||
- [How do I deploy to GitHub Pages? - Vue Router](./github-pages#vue-router) | ||
|
||
Some hosting providers let you provide a catch-all file that will be returned instead of a 404. For example, [Surge](https://surge.sh/help/adding-a-200-page-for-client-side-routing) uses a file called `200.html` for that purpose. If you're using a hosting provider that works that way, you could rename `index.html` to `200.html` as part of the build. | ||
|
||
## Using a relative path for `base` | ||
|
||
A misconfigured production server is easily the most common cause of the symptoms described above, but they can also be caused by using a relative path for Vite's `base` setting. | ||
|
||
The `base` is used as the starting point for all URLs in the built application. By default, it is set to `/`, but it can be changed if you want to deploy your application to a specific path. | ||
|
||
For example, by default you'll have a line similar to this in `dist/index.html`: | ||
|
||
```html | ||
<script type="module" src="/assets/index-94c19c52.js"></script> | ||
``` | ||
|
||
That path starts with `/`, which is the `base`. | ||
|
||
But say you want to deploy your application to `http://example.com/my-app/`. The `my-app` here isn't part of the route path, it's the base path. The contents of the built `dist` folder are being served up from that path. | ||
|
||
To get this to work you'd need to configure `base: '/my-app/'` in your Vite configuration. This will then be used as a prefix for URLs. For example, in `dist/index.html` there should be a line like this: | ||
|
||
```html | ||
<script type="module" src="/my-app/assets/index-94c19c52.js"></script> | ||
``` | ||
|
||
You might think that you could solve the problem using a relative path instead, like `base: './'`. This can be very tempting, as at first glance it seems to allow the application to run at any path, without needing to know the deployment path at build-time. | ||
|
||
Vite does support using a relative path, but in practice this is where things can go wrong if you're also using `createWebHistory()`. | ||
|
||
Let's consider the same route paths from the earlier example: | ||
|
||
``` | ||
http://example.com/ | ||
http://example.com/products | ||
http://example.com/products/search | ||
``` | ||
|
||
The first two URLs will likely work fine, but the third one will explode. | ||
|
||
Why? Because `dist/index.html` is trying to use a path relative to the route path: | ||
|
||
```html | ||
<script type="module" src="./assets/index-94c19c52.js"></script> | ||
``` | ||
|
||
The browser doesn't know that this line is in `dist/index.html`. As far as the browser is concerned, that's just a line in the page `http://example.com/products/search`. It'll resolve that relative path to `http://example.com/products/assets/index-94c19c52.js`, with the extra `/products` part still in the URL. That'll fail, as it isn't the correct location for that file. | ||
|
||
While it is theoretically possible to configure the server to handle this, it's usually better just to avoid using a relative path for `base`. | ||
|
||
See <https://router.vuejs.org/guide/essentials/history-mode.html> for details. | ||
If you're using the old Vue CLI, the equivalent of `base` is [`publicPath`](https://cli.vuejs.org/config/#publicpath). This also supports using a relative path, with much the same problems. |