Permanently redirect to /s/... for share links (#4067)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
app/utils/routeHelpers.test.ts
Normal file
14
app/utils/routeHelpers.test.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user