Permanently redirect to /s/... for share links (#4067)

This commit is contained in:
Apoorv Mishra
2022-09-08 13:14:25 +05:30
committed by GitHub
parent c36dcc9712
commit 97f70edd93
12 changed files with 65 additions and 19 deletions

View File

@@ -25,6 +25,7 @@ import { NotFoundError } from "~/utils/errors";
import { uploadFile } from "~/utils/files";
import history from "~/utils/history";
import { isModKey } from "~/utils/keyboard";
import { sharedDocumentPath } from "~/utils/routeHelpers";
import { isHash } from "~/utils/urls";
import DocumentBreadcrumb from "./DocumentBreadcrumb";
@@ -160,7 +161,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
}
if (shareId) {
navigateTo = `/share/${shareId}${navigateTo}`;
navigateTo = sharedDocumentPath(shareId, navigateTo);
}
history.push(navigateTo);

View File

@@ -7,6 +7,7 @@ import breakpoint from "styled-components-breakpoint";
import Document from "~/models/Document";
import Highlight, { Mark } from "~/components/Highlight";
import { hover } from "~/styles";
import { sharedDocumentPath } from "~/utils/routeHelpers";
type Props = {
document: Document;
@@ -38,7 +39,9 @@ function DocumentListItem(
ref={ref}
dir={document.dir}
to={{
pathname: shareId ? `/share/${shareId}${document.url}` : document.url,
pathname: shareId
? sharedDocumentPath(shareId, document.url)
: document.url,
state: {
title: document.titleWithDefault,
},

View File

@@ -5,6 +5,7 @@ import Collection from "~/models/Collection";
import Document from "~/models/Document";
import useStores from "~/hooks/useStores";
import { NavigationNode } from "~/types";
import { sharedDocumentPath } from "~/utils/routeHelpers";
import Disclosure from "./Disclosure";
import SidebarLink from "./SidebarLink";
@@ -92,7 +93,7 @@ function DocumentLink(
<>
<SidebarLink
to={{
pathname: `/share/${shareId}${node.url}`,
pathname: sharedDocumentPath(shareId, node.url),
state: {
title: node.title,
},

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { Switch } from "react-router-dom";
import { Switch, Redirect } from "react-router-dom";
import DelayedMount from "~/components/DelayedMount";
import FullscreenLoading from "~/components/FullscreenLoading";
import Route from "~/components/ProfiledRoute";
@@ -55,10 +55,17 @@ export default function Routes() {
<Route exact path="/create" component={Login} />
<Route exact path="/logout" component={Logout} />
<Route exact path="/share/:shareId" component={SharedDocument} />
<Redirect exact from="/share/:shareId" to="/s/:shareId" />
<Route exact path="/s/:shareId" component={SharedDocument} />
<Redirect
exact
from={`/share/:shareId/doc/${slug}`}
to={`/s/:shareId/doc/${slug}`}
/>
<Route
exact
path={`/share/:shareId/doc/${slug}`}
path={`/s/:shareId/doc/${slug}`}
component={SharedDocument}
/>

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import Breadcrumb from "~/components/Breadcrumb";
import { MenuInternalLink, NavigationNode } from "~/types";
import { sharedDocumentPath } from "~/utils/routeHelpers";
type Props = {
documentId: string;
@@ -48,7 +49,11 @@ const PublicBreadcrumb: React.FC<Props> = ({
pathToDocument(sharedTree, documentId)
.slice(0, -1)
.map((item) => {
return { ...item, type: "route", to: `/share/${shareId}${item.url}` };
return {
...item,
type: "route",
to: sharedDocumentPath(shareId, item.url),
};
}),
[sharedTree, shareId, documentId]
);

View File

@@ -9,6 +9,7 @@ import EmojiIcon from "~/components/EmojiIcon";
import Flex from "~/components/Flex";
import { hover } from "~/styles";
import { NavigationNode } from "~/types";
import { sharedDocumentPath } from "~/utils/routeHelpers";
type Props = {
shareId?: string;
@@ -64,7 +65,9 @@ function ReferenceListItem({
return (
<DocumentLink
to={{
pathname: shareId ? `/share/${shareId}${document.url}` : document.url,
pathname: shareId
? sharedDocumentPath(shareId, document.url)
: document.url,
hash: anchor ? `d-${anchor}` : undefined,
state: {
title: document.title,

View File

@@ -0,0 +1,14 @@
import { sharedDocumentPath } from "./routeHelpers";
describe("#sharedDocumentPath", () => {
test("should return share path for a document", () => {
const shareId = "1c922644-40d8-41fe-98f9-df2b67239d45";
const docPath = "/doc/test-DjDlkBi77t";
expect(sharedDocumentPath(shareId)).toBe(
"/s/1c922644-40d8-41fe-98f9-df2b67239d45"
);
expect(sharedDocumentPath(shareId, docPath)).toBe(
"/s/1c922644-40d8-41fe-98f9-df2b67239d45/doc/test-DjDlkBi77t"
);
});
});

View File

@@ -117,6 +117,10 @@ export function searchPath(
return `${route}${search}`;
}
export function sharedDocumentPath(shareId: string, docPath?: string) {
return docPath ? `/s/${shareId}${docPath}` : `/s/${shareId}`;
}
export function notFoundUrl(): string {
return "/404";
}

View File

@@ -92,7 +92,7 @@ class Share extends IdModel {
}
get canonicalUrl() {
return `${this.team.url}/share/${this.id}`;
return `${this.team.url}/s/${this.id}`;
}
// associations

View File

@@ -8,7 +8,7 @@ export default function present(share: Share, isAdmin = false) {
documentTitle: share.document?.title,
documentUrl: share.document?.url,
published: share.published,
url: `${share.team.url}/share/${share.id}`,
url: share.canonicalUrl,
createdBy: presentUser(share.user),
includeChildDocuments: share.includeChildDocuments,
lastAccessedAt: share.lastAccessedAt || undefined,

View File

@@ -8,19 +8,19 @@ afterAll(server.disconnect);
beforeEach(db.flush);
describe("/share/:id", () => {
describe("/s/:id", () => {
it("should return standard title in html when loading unpublished share", async () => {
const share = await buildShare({
published: false,
});
const res = await server.get(`/share/${share.id}`);
const res = await server.get(`/s/${share.id}`);
const body = await res.text();
expect(res.status).toEqual(404);
expect(body).toContain("<title>Outline</title>");
});
it("should return standard title in html when share does not exist", async () => {
const res = await server.get(`/share/junk`);
const res = await server.get(`/s/junk`);
const body = await res.text();
expect(res.status).toEqual(404);
expect(body).toContain("<title>Outline</title>");
@@ -33,7 +33,7 @@ describe("/share/:id", () => {
teamId: document.teamId,
});
await document.destroy();
const res = await server.get(`/share/${share.id}`);
const res = await server.get(`/s/${share.id}`);
const body = await res.text();
expect(res.status).toEqual(404);
expect(body).toContain("<title>Outline</title>");
@@ -45,7 +45,7 @@ describe("/share/:id", () => {
documentId: document.id,
teamId: document.teamId,
});
const res = await server.get(`/share/${share.id}`);
const res = await server.get(`/s/${share.id}`);
const body = await res.text();
expect(res.status).toEqual(200);
expect(body).toContain(`<title>${document.title}</title>`);
@@ -57,7 +57,7 @@ describe("/share/:id", () => {
documentId: document.id,
teamId: document.teamId,
});
const res = await server.get(`/share/${share.id}/doc/${document.urlId}`);
const res = await server.get(`/s/${share.id}/doc/${document.urlId}`);
const body = await res.text();
expect(res.status).toEqual(200);
expect(body).toContain(`<title>${document.title}</title>`);

View File

@@ -25,6 +25,14 @@ koa.use(
koa.use<BaseContext, UserAgentContext>(userAgent);
router.use(
["/share/:shareId", "/share/:shareId/doc/:documentSlug", "/share/:shareId/*"],
(ctx) => {
ctx.redirect(ctx.path.replace(/^\/share/, "/s"));
ctx.status = 301;
}
);
if (isProduction) {
router.get("/static/*", async (ctx) => {
try {
@@ -84,9 +92,9 @@ router.get("/opensearch.xml", (ctx) => {
ctx.body = opensearchResponse(ctx.request.URL.origin);
});
router.get("/share/:shareId", renderShare);
router.get("/share/:shareId/doc/:documentSlug", renderShare);
router.get("/share/:shareId/*", renderShare);
router.get("/s/:shareId", renderShare);
router.get("/s/:shareId/doc/:documentSlug", renderShare);
router.get("/s/:shareId/*", renderShare);
// catch all for application
router.get("*", renderApp);