Repository: vuejs/vue
Pull request: fix(server): use path.posix.join to generate public path, fix #8167

fix #8167 

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [ ] New/updated tests are included

Changed files:
filename: src/server/template-renderer/index.js
patch: @@ -60,7 +60,7 @@ export default class TemplateRenderer {
     // extra functionality with client manifest
     if (options.clientManifest) {
       const clientManifest = this.clientManifest = options.clientManifest
-      this.publicPath = clientManifest.publicPath.replace(/\/$/, '')
+      this.publicPath = clientManifest.publicPath
       // preload/prefetch directives
       this.preloadFiles = (clientManifest.initial || []).map(normalizeFile)
       this.prefetchFiles = (clientManifest.async || []).map(normalizeFile)
@@ -114,7 +114,7 @@ export default class TemplateRenderer {
     return (
       // render links for css files
       (cssFiles.length
-        ? cssFiles.map(({ file }) => `<link rel="stylesheet" href="${this.publicPath}/${file}">`).join('')
+        ? cssFiles.map(({ file }) => `<link rel="stylesheet" href="${this.getPublicPath(file)}">`).join('')
         : '') +
       // context.styles is a getter exposed by vue-style-loader which contains
       // the inline component styles collected during SSR
@@ -153,7 +153,7 @@ export default class TemplateRenderer {
           extra = ` type="font/${extension}" crossorigin`
         }
         return `<link rel="preload" href="${
-          this.publicPath}/${file
+          this.getPublicPath(file)
         }"${
           asType !== '' ? ` as="${asType}"` : ''
         }${
@@ -179,7 +179,7 @@ export default class TemplateRenderer {
         if (alreadyRendered(file)) {
           return ''
         }
-        return `<link rel="prefetch" href="${this.publicPath}/${file}">`
+        return `<link rel="prefetch" href="${this.getPublicPath(file)}">`
       }).join('')
     } else {
       return ''
@@ -206,7 +206,7 @@ export default class TemplateRenderer {
       const async = (this.getUsedAsyncFiles(context) || []).filter(({ file }) => isJS(file))
       const needed = [initial[0]].concat(async || [], initial.slice(1))
       return needed.map(({ file }) => {
-        return `<script src="${this.publicPath}/${file}" defer></script>`
+        return `<script src="${this.getPublicPath(file)}" defer></script>`
       }).join('')
     } else {
       return ''
@@ -228,6 +228,10 @@ export default class TemplateRenderer {
     }
     return new TemplateStream(this, this.parsedTemplate, context || {})
   }
+
+  getPublicPath (file: string) {
+    return path.posix.join(this.publicPath, file)
+  }
 }
 
 function normalizeFile (file: string): Resource {

filename: test/ssr/ssr-template.spec.js
patch: @@ -11,6 +11,7 @@ function generateClientManifest (file, cb) {
   compileWithWebpack(file, {
     output: {
       path: '/',
+      publicPath: '/',
       filename: '[name].js'
     },
     optimization: {