diff --git a/site/src/pages/TemplatePage/TemplateRedirectController.test.tsx b/site/src/pages/TemplatePage/TemplateRedirectController.test.tsx
new file mode 100644
index 0000000000000..459e94fe911f0
--- /dev/null
+++ b/site/src/pages/TemplatePage/TemplateRedirectController.test.tsx
@@ -0,0 +1,40 @@
+import { waitFor } from "@testing-library/react";
+import { API } from "api/api";
+import * as M from "testHelpers/entities";
+import { renderWithAuth } from "testHelpers/renderHelpers";
+import { TemplateRedirectController } from "./TemplateRedirectController";
+
+const renderTemplateRedirectController = (route: string) => {
+ return renderWithAuth(, {
+ route,
+ path: "/templates/:organization?/:template",
+ });
+};
+
+it("redirects from multi-org to single-org", async () => {
+ const { router } = renderTemplateRedirectController(
+ `/templates/${M.MockTemplate.organization_name}/${M.MockTemplate.name}`,
+ );
+
+ await waitFor(() =>
+ expect(router.state.location.pathname).toEqual(
+ `/templates/${M.MockTemplate.name}`,
+ ),
+ );
+});
+
+it("redirects from single-org to multi-org", async () => {
+ jest
+ .spyOn(API, "getOrganizations")
+ .mockResolvedValueOnce([M.MockDefaultOrganization, M.MockOrganization2]);
+
+ const { router } = renderTemplateRedirectController(
+ `/templates/${M.MockTemplate.name}`,
+ );
+
+ await waitFor(() =>
+ expect(router.state.location.pathname).toEqual(
+ `/templates/${M.MockDefaultOrganization.name}/${M.MockTemplate.name}`,
+ ),
+ );
+});
diff --git a/site/src/pages/TemplatePage/TemplateRedirectController.tsx b/site/src/pages/TemplatePage/TemplateRedirectController.tsx
new file mode 100644
index 0000000000000..66da3b6ea0bab
--- /dev/null
+++ b/site/src/pages/TemplatePage/TemplateRedirectController.tsx
@@ -0,0 +1,53 @@
+import type { FC } from "react";
+import { Navigate, Outlet, useLocation, useParams } from "react-router-dom";
+import type { Organization } from "api/typesGenerated";
+import { useDashboard } from "modules/dashboard/useDashboard";
+
+export const TemplateRedirectController: FC = () => {
+ const { organizations, showOrganizations } = useDashboard();
+ const { organization, template } = useParams() as {
+ organization?: string;
+ template: string;
+ };
+ const location = useLocation();
+
+ // We redirect templates without an organization to the default organization,
+ // as that's likely what any links floating around expect.
+ if (showOrganizations && !organization) {
+ const extraPath = removePrefix(location.pathname, `/templates/${template}`);
+
+ return (
+
+ );
+ }
+
+ // `showOrganizations` can only be false when there is a single organization,
+ // so it's safe to throw away the organization name.
+ if (!showOrganizations && organization) {
+ const extraPath = removePrefix(
+ location.pathname,
+ `/templates/${organization}/${template}`,
+ );
+
+ return (
+
+ );
+ }
+
+ return ;
+};
+
+const getOrganizationNameByDefault = (organizations: Organization[]) =>
+ organizations.find((org) => org.is_default)?.name;
+
+// I really hate doing it this way, but React Router does not provide a better way.
+const removePrefix = (self: string, prefix: string) =>
+ self.startsWith(prefix) ? self.slice(prefix.length) : self;
diff --git a/site/src/router.tsx b/site/src/router.tsx
index 152c458b83fb4..75091a311cb3a 100644
--- a/site/src/router.tsx
+++ b/site/src/router.tsx
@@ -1,4 +1,4 @@
-import { Suspense, lazy } from "react";
+import { lazy, Suspense } from "react";
import {
createBrowserRouter,
createRoutesFromChildren,
@@ -6,6 +6,7 @@ import {
Outlet,
Route,
} from "react-router-dom";
+import { TemplateRedirectController } from "pages/TemplatePage/TemplateRedirectController";
import { Loader } from "./components/Loader/Loader";
import { RequireAuth } from "./contexts/auth/RequireAuth";
import { DashboardLayout } from "./modules/dashboard/DashboardLayout";
@@ -289,27 +290,29 @@ const RoutesWithSuspense = () => {
const templateRouter = () => {
return (
- }>
- } />
- } />
- } />
- } />
- } />
- } />
-
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
- } />
+ } />
- }>
- } />
- } />
- } />
- } />
-
+ }>
+ } />
+ } />
+ } />
+ } />
+
-
-
- } />
+
+
+ } />
+