vuejs / vitepress

Vite & Vue powered static site generator.
https://vitepress.dev
MIT License
13.07k stars 2.11k forks source link

Client-side redirects #4160

Open Veetaha opened 2 months ago

Veetaha commented 2 months ago

Is your feature request related to a problem? Please describe.

I'm using Vitepress to document my Rust library called bon. I'm hosting it on Github Pages, which is quite a limited hosting provider, but it's a simple one and it's built into Github, so it's very convenient. Github Pages doesn't support server-side redirects, however, VitePress could just do redirects client-side.

I'm preparing a 2.0 release of my library. I'm organizing different versions of the docs under v1/ and v2/ path prefixes. However, I'd like to have a special prefix (e.g. latest/) that would forward to the latest version of the docs, such that I could use this link in my blog posts, for example. The version-specific URLs are used the doc-comments of my library.

Or.. ideally, prefix-less paths should forward to the latest version of the docs automatically. A similar thing is implemented on docs.rs (the default site that generates docs for Rust libraries):

I want to have the same behaviour with Vitepress where

Describe the solution you'd like

There could be client-side redirects config in Vitepress config.mts file. For example, an object with glob patterns:

{
     "/guide/*": "/v2/guide/${1}",
     "/reference/*": "/v2/reference/${1}"
}

(inspired by Cloudflare's routing rules)

Describe alternatives you've considered

Only workarounds:

Additional context

I'm weak at frontend, and especially Vue, but I used other similar frontend frameworks ~6 years ago at university. So I'm seeking for a simple solution to this problem that doesn't involve writing a lot of Vue code or moving to another static page hosting. I adore the current VitePress design and simplicity :heart:

Validations

brc-dd commented 2 months ago

Easiest way would be to just use symlinks:

// .vitepress/config.ts

export default defineConfig({
  vite: {
    resolve: {
      preserveSymlinks: true
    }
  }
})
ln -s v2/guide/ ./guide
ln -s v2/reference ./reference

This won't redirect the page though. But your v2 content will be available without prefix.

Veetaha commented 2 months ago

Thank you for the suggestion! I've been thinking of using symlinks as well, although using them comes with some drawbacks for the doc-tests automation in my repo, and cross-platformness (if you clone the repo on Windows, symlinks will be just regular files with a path in them).

However, I suppose it'll be easier to do it this way than migrating to Cloudflare pages. This workaround will probably suffice, but I'll keep this issue open for the built-in client-side redirects feature request.

brc-dd commented 2 months ago

Without symlinks, one of the way is just 404 it and redirect it from there. Because for general stuff it can't be supported properly (for example you want any request to /guide/* serves v2/guide/index.html, then for it to work without 404 status in gh pages, you'll need to generate infinite combinations of pathnames as separate html files). docs.rs is also doing server-side redirect.

Example code:

<!-- .vitepress/theme/Layout.vue -->

<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { inBrowser, useData, useRouter } from 'vitepress'
import { watch } from 'vue'

const { page } = useData()
const { go } = useRouter()

const redirects = Object.entries({
  '/latest/': '/v2/',
  '/guide/': '/v2/guide/',
  '/reference/': '/v2/reference/'
})

watch(
  () => page.value.isNotFound,
  (isNotFound) => {
    if (!isNotFound || !inBrowser) return
    const redirect = redirects.find(([from]) => window.location.pathname.startsWith(from))
    if (!redirect) return
    go(redirect[1] + window.location.pathname.slice(redirect[0].length))
  },
  { immediate: true }
)
</script>

<template>
  <DefaultTheme.Layout />
</template>
// .vitepress/theme/index.ts

import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'

export default { ...DefaultTheme, Layout }

AFAICT using 404.html is the only solution other than maybe re-writing the router to support hash-based navigation. Don't use this if your application is SEO-critical as the page will still be returned with a 404 status code.

Veetaha commented 2 months ago

Aha, I see so when Github Pages doesn't find the corresponding .html file for the path it returns a 404 status with 404.html. And then the client-side JS could redirect the user to the correct page. Yeah, it sounds like a hack, because there is an intermediate 404 status involved.

So now having client-side redirects sounds more like a hack than a feature :eyes:. I suppose such a thing is indeed the responsibility of the server side. Feel free to close this issue if such a hack shouldn't be in scope of VitePress. Although, I think this explanation may be part of the Vitepress docs

Veetaha commented 2 months ago

https://github.com/vuejs/vitepress/issues/4160#issuecomment-2308509400

Thank you for the code example! I was somehow thinking that Vitepress generated a single HTML for the entire build (since it's an SPA). So I was thinking client-side routing would be an obvious thing to implement in such case.

But I forgot about 404 status code in HTTP response completely, and now I see in the dist a bunch of different HTML files. I suppose Vitepress lazy-loads them under the hood without the page refresh.

brc-dd commented 2 months ago

Ah yeah, https://www.youtube.com/watch?v=xXrhg26VCSc (around 46 minute mark)

admirsaheta commented 1 month ago

Without symlinks, one of the way is just 404 it and redirect it from there. Because for general stuff it can't be supported properly (for example you want any request to /guide/* serves v2/guide/index.html, then for it to work without 404 status in gh pages, you'll need to generate infinite combinations of pathnames as separate html files). docs.rs is also doing server-side redirect.

Example code:

<!-- .vitepress/theme/Layout.vue -->

<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { inBrowser, useData, useRouter } from 'vitepress'
import { watch } from 'vue'

const { page } = useData()
const { go } = useRouter()

const redirects = Object.entries({
  '/latest/': '/v2/',
  '/guide/': '/v2/guide/',
  '/reference/': '/v2/reference/'
})

watch(
  () => page.value.isNotFound,
  (isNotFound) => {
    if (!isNotFound || !inBrowser) return
    const redirect = redirects.find(([from]) => window.location.pathname.startsWith(from))
    if (!redirect) return
    go(redirect[1] + window.location.pathname.slice(redirect[0].length))
  },
  { immediate: true }
)
</script>

<template>
  <DefaultTheme.Layout />
</template>
// .vitepress/theme/index.ts

import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'

export default { ...DefaultTheme, Layout }

AFAICT using 404.html is the only solution other than maybe re-writing the router to support hash-based navigation. Don't use this if your application is SEO-critical as the page will still be returned with a 404 status code.

This worked for me! Thanks

olets commented 1 week ago

Another use case for client-side redirects: I want to reorganize an established docs site. I don't want existing links to break (I can update the ones I control, but can't update people's bookmarks or browser history entries). I'd like to be able to

export default defineConfig({
  redirects: {
    '/old': '/new',
  },
})

Would be wonderful to have a config with a shape similar to Astro's redirects https://docs.astro.build/en/reference/configuration-reference/#redirects

brc-dd commented 1 week ago

Hmm, astro supports redirects which can be determined statically. We can also support those. But wildcard redirects cannot be easily supported in a SSG 👀

Veetaha commented 4 days ago

I've since updated my docs website to a custom domain managed by Cloudflare. I still deploy to Github Pages, but I now have a Cloudflare proxy server in the middle.

Here is how you can achieve the same:

  1. Register an account on Cloudflare or use an existing one. ⚠️ I tried Namecheap before Cloudflare, and wasted $15 on their scam that they call "handshake domains". Don't repeat my mistake! Use Cloudflare, it's much better and feature-rich
  2. Purchase a custom domain name of your taste.
  3. Configure your apex domain records to forward traffic to Github Pages according to the Github's docs. Example config as per Github's docs that works for me:
    • 4 A records: image
    • 4 AAAA records and a CNAME: image
  4. Update your Github repo settings to use the custom domain: image
  5. Add the file named CNAME with a single line of the custom domain name to your /public dir in Vitepress project dir. I'm not sure if this step is required. It may be required if you are deploying to Github Pages with the old approach (not via the Github Actions artifacts).
  6. Cloudflare puts a proxy server in front of your domain by default. This allows you to configure custom redirects. For example, here is how I configured a /discord redirect to my discord server: image The redirect rules are very flexible, see the docs here.
  7. Optional. You can use terraform to configure the redirects and keep your config in code (IAC). See an example in Cloudflare's docs

@brc-dd the Cloudflare solution above works for me and I think client-side redirects are more of an antipattern at this point. I'm inclined to close this issue as a "wontfix" unless you think otherwise.

olets commented 4 days ago

I'm inclined to close this issue as a "wontfix"

I hope this stays open. A solution for redirects which doesn't take more dev chops or budget than the rest of out-of-the-box VitePress would be nice.

//

wildcard redirects cannot be easily supported in a SSG

I haven't looked at Astro's implementation, and don't have a personal need for wildcard redirects currently, and don't need to see a browser wars-style "x has it so y should have it", and conceptually at least Astro's doesn't seem that tricky. ({ 'x/[slug]': 'y/[slug]' } => during build, if the output path regex matches y/[slug] then configure a redirect to it from x/[slug]).

But hardcoded static redirects would be a great advancement over no client-side redirects.