Fix issues where the path did not have a trailing slash

9e4c7e02ff21fcbdf0ae618970c95f9f0b8e0751

Tucker McKnight <tmcknight@instructure.com> | Mon Aug 10 2026

Fix issues where the path did not have a trailing slash

Makes the path variable more consistent -- should either just be a
slash if the pages are going at the root, or will be like
/path/ if it's going in a subdirectory.

Makes the branches and tags pages use the nav helper.
getConfig.ts:74
Before
73
74
75









76
  }

  reposConfiguration['repos'] = repos
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
  return reposConfiguration
}
After
73
74
75
76
77
78
79
80
81
82
83
84
85
  }

  reposConfiguration['repos'] = repos

  // Special fixup on the path value, since we expect it
  // to always start and end with a slash (or just be a slash).
  if (!reposConfiguration.path.endsWith('/')) {
    reposConfiguration.path = reposConfiguration.path + '/'
  }
  if (!reposConfiguration.path.startsWith('/')) {
    reposConfiguration.path = '/' + reposConfiguration.path
  }
  return reposConfiguration
}
js_templates/branches.ts:24
Before
23
24
25
26
27
28
      return branch.repoName === data.flatRef.repoName ?
        m('li', [
          m('a', {
            href: `${data.reposPath}/${slugify(branch.repoName)}/branch/${slugify(branch.refName)}/branches`
          }, branch.refName),
          branch.refName === data.flatRef.refName
            ? m('div', {class: "badge rounded-pill bg-secondary mx-1"}, 'current') : null,
After
23
24
25
26
27
28
      return branch.repoName === data.flatRef.repoName ?
        m('li', [
          m('a', {
            href: nav.branches({refName: branch.refName, refType: 'branch'})
          }, branch.refName),
          branch.refName === data.flatRef.refName
            ? m('div', {class: "badge rounded-pill bg-secondary mx-1"}, 'current') : null,
js_templates/common/commonPage.ts:4
Before
3
4
5
6
7
8
export default async (template, reposConfiguration, eleventyConfig) => {
  return async (data) => {
    const opts = await template(reposConfiguration, eleventyConfig, data)
    const rootPath = opts.rootPath == '/' ? '' : opts.rootPath
    
    return render([
      m.trust('<!doctype html>'),
After
3
4
5
6

7
export default async (template, reposConfiguration, eleventyConfig) => {
  return async (data) => {
    const opts = await template(reposConfiguration, eleventyConfig, data)
    
⁣
    return render([
      m.trust('<!doctype html>'),
js_templates/common/commonPage.ts:13
Before
12
13
14
15
16
17
18
19
          m('meta', {charset: "utf-8"}),
          m('meta', {name: "viewport", content: "width=device-width, initial-scale=1"}),
          m('title', opts.pageTitle),
          m('link', {rel: "stylesheet", href: `${rootPath}/css/design-board.css`}),
          opts.additionalHeadContent || null,
          m('script', {src: `${rootPath}/frontend/top.js`}),
        ]),
        m('body', [
          m('div', {class: "container-fluid container-lg"}, [
After
12
13
14
15
16
17
18
19
          m('meta', {charset: "utf-8"}),
          m('meta', {name: "viewport", content: "width=device-width, initial-scale=1"}),
          m('title', opts.pageTitle),
          m('link', {rel: "stylesheet", href: `${opts.rootPath}css/design-board.css`}),
          opts.additionalHeadContent || null,
          m('script', {src: `${opts.rootPath}frontend/top.js`}),
        ]),
        m('body', [
          m('div', {class: "container-fluid container-lg"}, [
js_templates/common/commonPage.ts:62
Before
61
62
63
64
65
66
            integrity: "sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI",
            crossorigin: "anonymous"
          }),
          m('script', {src: `${rootPath}/frontend/main-frontend.bundle.js`})
        ])
      ])
    ])
After
61
62
63
64
65
66
            integrity: "sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI",
            crossorigin: "anonymous"
          }),
          m('script', {src: `${opts.rootPath}frontend/main-frontend.bundle.js`})
        ])
      ])
    ])
js_templates/common/htmlPage.ts:56
Before
55
56
57
58
59
60
        rel: "stylesheet",
        id: "prism-theme",
        type: "text/css",
        href: `${data.reposPath}/vendor/prism.css`
      }),
    ],
    navbarContent: m('nav', {class: "navbar navbar-expand flex-wrap"}, [
After
55
56
57
58
59
60
        rel: "stylesheet",
        id: "prism-theme",
        type: "text/css",
        href: `${nav.rootPath()}vendor/prism.css`
      }),
    ],
    navbarContent: m('nav', {class: "navbar navbar-expand flex-wrap"}, [
js_templates/feed.ts:16
Before
15
16
17

18
19

    const slugify = eleventyConfig.getFilter("slugify")

⁣
    return render([
      m.trust('<?xml version="1.0" encoding="utf-8"?>'),
      m('feed', {xmlns: "http://www.w3.org/2005/Atom"}, [
After
15
16
17
18
19
20

    const slugify = eleventyConfig.getFilter("slugify")

    // TODO: use nav for baseUrl + /repos/ part below
    return render([
      m.trust('<?xml version="1.0" encoding="utf-8"?>'),
      m('feed', {xmlns: "http://www.w3.org/2005/Atom"}, [
js_templates/helpers/nav.ts:21
Before
20
21
22

23
24

25
26
27
28
29
30
31
32
  // These two aren't actually used by any pages, but they're in almost
  // every page URL. E.g. all of them start with 'repos/my-repo-name'
  // or 'repos/my-repo-name/branches.'
⁣
  const repoBasePath = `${reposPath}/${args.slugify(args.currentRepoName)}`

⁣
  const refPath = (refName, refType) => {
    if (refType === 'commit'
        || (refType === 'branch' && refName === args.reposConfig.repos[args.currentRepoName].defaultBranch)) {
      return repoBasePath
    }
    return `${repoBasePath}/${refType}/${args.slugify(refName)}`
  }

  type RefArgument = {
After
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  // These two aren't actually used by any pages, but they're in almost
  // every page URL. E.g. all of them start with 'repos/my-repo-name'
  // or 'repos/my-repo-name/branches.'
  const repoBasePath = (repoName: string | null = null) => {
    return `${reposPath}${args.slugify(repoName || args.currentRepoName)}`
  }

  const refPath = (refName, refType) => {
    if (refType === 'commit'
        || (refType === 'branch' && refName === args.reposConfig.repos[args.currentRepoName].defaultBranch)) {
      return repoBasePath()
    }
    return `${repoBasePath()}/${refType}/${args.slugify(refName)}`
  }

  type RefArgument = {
js_templates/helpers/nav.ts:38
Before
37
38
39
40
41
42
43
44
45
46
47


48
49
50

  return {
    rootPath: () => {
      if (reposPath === "") {
        return "/"
      }
      else {
        return reposPath + '/'
      }
    },
    repoHomePath: () => {
⁣
⁣
      return repoBasePath
    },
    refHome: (newRef: RefArgument = null) => {
      const refName = newRef?.refName || args.currentRefName
After
37
38
39




40

41
42
43
44
45
46
47

  return {
    rootPath: () => {
⁣
⁣
⁣
⁣
      return reposPath
⁣
    },
    repoHomePath: (newRepo: string | null = null) => {
      // defaults to the currently-initialized repo (args.currentRepoName)
      // if newRepo is null
      return repoBasePath(newRepo)
    },
    refHome: (newRef: RefArgument = null) => {
      const refName = newRef?.refName || args.currentRefName
js_templates/helpers/nav.ts:83
Before
82
83
84
85
86
87
      return `${refPath(refName, refType)}/files`
    },
    commit: (hash: string) => {
      return `${repoBasePath}/commits/${hash}`
    },
    commits: (pageNum: number, newRef: RefArgument = null) => {
      const refName = newRef?.refName || args.currentRefName
After
82
83
84
85
86
87
      return `${refPath(refName, refType)}/files`
    },
    commit: (hash: string) => {
      return `${repoBasePath()}/commits/${hash}`
    },
    commits: (pageNum: number, newRef: RefArgument = null) => {
      const refName = newRef?.refName || args.currentRefName
js_templates/helpers/nav.ts:109
Before
108
109
110

111

112
113

      return `${refPath(refName, refType)}/commits.xml`
    },
⁣
    homepageButtons: args.reposConfig.repos[args.currentRepoName].defaultTemplate.homepageButtons
⁣
  }
}
After
108
109
110
111
112
113
114
115

      return `${refPath(refName, refType)}/commits.xml`
    },
    homepageButtons: () => {
      return args.reposConfig.repos[args.currentRepoName].defaultTemplate.homepageButtons
    }
  }
}
js_templates/index.ts:1
Before
0
1
2

3
4
5








6
7
import m from 'mithril'
import render from 'mithril-node-render'

⁣
export default (_reposConfig: any, eleventyConfig: any, data: any) => {
  const slugify = eleventyConfig.getFilter("slugify")

⁣
⁣
⁣
⁣
⁣
⁣
⁣
⁣
  const pageContent = m('div', {class: "container"}, [
    m('div', {class: "row my-3"},
      m('div', {class: "col"},
After
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import m from 'mithril'
import render from 'mithril-node-render'
import { NavHelper } from './helpers/nav.ts'

export default (reposConfig: any, eleventyConfig: any, data: any) => {
  const slugify = eleventyConfig.getFilter("slugify")

  const nav = NavHelper({
    reposConfig,
    slugify,
    currentRepoName: '',
    currentRefName: '',
    currentRefType: 'commit',
  })

  const pageContent = m('div', {class: "container"}, [
    m('div', {class: "row my-3"},
      m('div', {class: "col"},
js_templates/index.ts:16
Before
15
16
17
18
19
20
21
22
23
24
25
26
27
          m('div', {class: "col", style: "flex-basis: 30rem;"},
            m('div', {class: "my-2 card bezel-gray flex-grow-1"},
              m('div', {class: "card-header"},
                m('a', {class: "card-title fs-5", href: `${data.reposPath}/${slugify(repo.name)}`}, repo.name)
              ),
              m('div', {class: "card-body"},
                repo.description ? m('p', {class: "card-text"}, repo.description) : null
              ),
              m('div', {class: "card-footer"}, [
                m('a', {
                  href: `${data.reposPath}/${slugify(repo.name)}`,
                  class: "ms-0 me-2 my-2 btn btn-primary text-white"
                }, 'Go to site'),
                m('button', {
After
15
16
17
18
19
20
21
22
23
24
25
26
27
          m('div', {class: "col", style: "flex-basis: 30rem;"},
            m('div', {class: "my-2 card bezel-gray flex-grow-1"},
              m('div', {class: "card-header"},
                m('a', {class: "card-title fs-5", href: nav.repoHomePath(repo.name)}, repo.name)
              ),
              m('div', {class: "card-body"},
                repo.description ? m('p', {class: "card-text"}, repo.description) : null
              ),
              m('div', {class: "card-footer"}, [
                m('a', {
                  href: nav.repoHomePath(repo.name),
                  class: "ms-0 me-2 my-2 btn btn-primary text-white"
                }, 'Go to site'),
                m('button', {
js_templates/index.ts:46
Before
45
46
47
48
49
50
  ])

  return {
    rootPath: `${data.reposPath}/`,
    pageTitle: 'Repositories',
    pageContent
  }
After
45
46
47
48
49
50
  ])

  return {
    rootPath: nav.rootPath(),
    pageTitle: 'Repositories',
    pageContent
  }
js_templates/repo.ts:126
Before
125
126
127
128
129
130
                    }, 'Copy' )
                  ]),
                  m('div', {class: "header-button-container"}, [
                    nav.homepageButtons.map((buttonConfig) => {
                      return m('a', {
                        class: "btn btn-secondary",
                        href: buttonConfig.url,
After
125
126
127
128
129
130
                    }, 'Copy' )
                  ]),
                  m('div', {class: "header-button-container"}, [
                    nav.homepageButtons().map((buttonConfig) => {
                      return m('a', {
                        class: "btn btn-secondary",
                        href: buttonConfig.url,
js_templates/tags.ts:23
Before
22
23
24
25
26
27
      return tag.repoName === data.flatRef.repoName ?
        m('li', [
          m('a', {
            href: `${data.reposPath}/${slugify(tag.repoName)}/tag/${slugify(tag.refName)}/tags`
          }, tag.refName),
          tag.refName === data.flatRef.refName
            ? m('div', {class: "badge rounded-pill bg-secondary mx-1"}, 'current') : null,
After
22
23
24
25
26
27
      return tag.repoName === data.flatRef.repoName ?
        m('li', [
          m('a', {
            href: nav.tags({refName: tag.refName, refType: 'tag'})
          }, tag.refName),
          tag.refName === data.flatRef.refName
            ? m('div', {class: "badge rounded-pill bg-secondary mx-1"}, 'current') : null,
main.ts:309
Before
308
309
310
311
312
313
  // INDEX.TS
  eleventyConfig.addTemplate(
    'repos/index.11ty.js',
    commonPage(indexJsTemplate, {}, eleventyConfig),
    {
      permalink: `${reposPath}/index.html`,
    }
After
308
309
310
311
312
313
  // INDEX.TS
  eleventyConfig.addTemplate(
    'repos/index.11ty.js',
    commonPage(indexJsTemplate, reposConfiguration, eleventyConfig),
    {
      permalink: `${reposPath}/index.html`,
    }
src/vcses/git/helpers.ts:1
Before
0
1
2
3
export default {
    cloneUrl: (baseUrl: string, repoName: string) => {
        return `${baseUrl}/${repoName.toLowerCase().replaceAll(" ", "-")}.git`
    }
}
After
0
1
2
3
export default {
    cloneUrl: (baseUrl: string, repoName: string) => {
        return `${baseUrl}${repoName.toLowerCase().replaceAll(" ", "-")}.git`
    }
}