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 { uploadFile } from "~/utils/files";
|
||||||
import history from "~/utils/history";
|
import history from "~/utils/history";
|
||||||
import { isModKey } from "~/utils/keyboard";
|
import { isModKey } from "~/utils/keyboard";
|
||||||
|
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||||
import { isHash } from "~/utils/urls";
|
import { isHash } from "~/utils/urls";
|
||||||
import DocumentBreadcrumb from "./DocumentBreadcrumb";
|
import DocumentBreadcrumb from "./DocumentBreadcrumb";
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shareId) {
|
if (shareId) {
|
||||||
navigateTo = `/share/${shareId}${navigateTo}`;
|
navigateTo = sharedDocumentPath(shareId, navigateTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
history.push(navigateTo);
|
history.push(navigateTo);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import breakpoint from "styled-components-breakpoint";
|
|||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import Highlight, { Mark } from "~/components/Highlight";
|
import Highlight, { Mark } from "~/components/Highlight";
|
||||||
import { hover } from "~/styles";
|
import { hover } from "~/styles";
|
||||||
|
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
document: Document;
|
document: Document;
|
||||||
@@ -38,7 +39,9 @@ function DocumentListItem(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
dir={document.dir}
|
dir={document.dir}
|
||||||
to={{
|
to={{
|
||||||
pathname: shareId ? `/share/${shareId}${document.url}` : document.url,
|
pathname: shareId
|
||||||
|
? sharedDocumentPath(shareId, document.url)
|
||||||
|
: document.url,
|
||||||
state: {
|
state: {
|
||||||
title: document.titleWithDefault,
|
title: document.titleWithDefault,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Collection from "~/models/Collection";
|
|||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import { NavigationNode } from "~/types";
|
import { NavigationNode } from "~/types";
|
||||||
|
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||||
import Disclosure from "./Disclosure";
|
import Disclosure from "./Disclosure";
|
||||||
import SidebarLink from "./SidebarLink";
|
import SidebarLink from "./SidebarLink";
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ function DocumentLink(
|
|||||||
<>
|
<>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to={{
|
to={{
|
||||||
pathname: `/share/${shareId}${node.url}`,
|
pathname: sharedDocumentPath(shareId, node.url),
|
||||||
state: {
|
state: {
|
||||||
title: node.title,
|
title: node.title,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Switch } from "react-router-dom";
|
import { Switch, Redirect } from "react-router-dom";
|
||||||
import DelayedMount from "~/components/DelayedMount";
|
import DelayedMount from "~/components/DelayedMount";
|
||||||
import FullscreenLoading from "~/components/FullscreenLoading";
|
import FullscreenLoading from "~/components/FullscreenLoading";
|
||||||
import Route from "~/components/ProfiledRoute";
|
import Route from "~/components/ProfiledRoute";
|
||||||
@@ -55,10 +55,17 @@ export default function Routes() {
|
|||||||
<Route exact path="/create" component={Login} />
|
<Route exact path="/create" component={Login} />
|
||||||
<Route exact path="/logout" component={Logout} />
|
<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
|
<Route
|
||||||
exact
|
exact
|
||||||
path={`/share/:shareId/doc/${slug}`}
|
path={`/s/:shareId/doc/${slug}`}
|
||||||
component={SharedDocument}
|
component={SharedDocument}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Breadcrumb from "~/components/Breadcrumb";
|
import Breadcrumb from "~/components/Breadcrumb";
|
||||||
import { MenuInternalLink, NavigationNode } from "~/types";
|
import { MenuInternalLink, NavigationNode } from "~/types";
|
||||||
|
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
@@ -48,7 +49,11 @@ const PublicBreadcrumb: React.FC<Props> = ({
|
|||||||
pathToDocument(sharedTree, documentId)
|
pathToDocument(sharedTree, documentId)
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
return { ...item, type: "route", to: `/share/${shareId}${item.url}` };
|
return {
|
||||||
|
...item,
|
||||||
|
type: "route",
|
||||||
|
to: sharedDocumentPath(shareId, item.url),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
[sharedTree, shareId, documentId]
|
[sharedTree, shareId, documentId]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import EmojiIcon from "~/components/EmojiIcon";
|
|||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import { hover } from "~/styles";
|
import { hover } from "~/styles";
|
||||||
import { NavigationNode } from "~/types";
|
import { NavigationNode } from "~/types";
|
||||||
|
import { sharedDocumentPath } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
@@ -64,7 +65,9 @@ function ReferenceListItem({
|
|||||||
return (
|
return (
|
||||||
<DocumentLink
|
<DocumentLink
|
||||||
to={{
|
to={{
|
||||||
pathname: shareId ? `/share/${shareId}${document.url}` : document.url,
|
pathname: shareId
|
||||||
|
? sharedDocumentPath(shareId, document.url)
|
||||||
|
: document.url,
|
||||||
hash: anchor ? `d-${anchor}` : undefined,
|
hash: anchor ? `d-${anchor}` : undefined,
|
||||||
state: {
|
state: {
|
||||||
title: document.title,
|
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}`;
|
return `${route}${search}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sharedDocumentPath(shareId: string, docPath?: string) {
|
||||||
|
return docPath ? `/s/${shareId}${docPath}` : `/s/${shareId}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function notFoundUrl(): string {
|
export function notFoundUrl(): string {
|
||||||
return "/404";
|
return "/404";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class Share extends IdModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get canonicalUrl() {
|
get canonicalUrl() {
|
||||||
return `${this.team.url}/share/${this.id}`;
|
return `${this.team.url}/s/${this.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// associations
|
// associations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function present(share: Share, isAdmin = false) {
|
|||||||
documentTitle: share.document?.title,
|
documentTitle: share.document?.title,
|
||||||
documentUrl: share.document?.url,
|
documentUrl: share.document?.url,
|
||||||
published: share.published,
|
published: share.published,
|
||||||
url: `${share.team.url}/share/${share.id}`,
|
url: share.canonicalUrl,
|
||||||
createdBy: presentUser(share.user),
|
createdBy: presentUser(share.user),
|
||||||
includeChildDocuments: share.includeChildDocuments,
|
includeChildDocuments: share.includeChildDocuments,
|
||||||
lastAccessedAt: share.lastAccessedAt || undefined,
|
lastAccessedAt: share.lastAccessedAt || undefined,
|
||||||
|
|||||||
@@ -8,19 +8,19 @@ afterAll(server.disconnect);
|
|||||||
|
|
||||||
beforeEach(db.flush);
|
beforeEach(db.flush);
|
||||||
|
|
||||||
describe("/share/:id", () => {
|
describe("/s/:id", () => {
|
||||||
it("should return standard title in html when loading unpublished share", async () => {
|
it("should return standard title in html when loading unpublished share", async () => {
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
published: false,
|
published: false,
|
||||||
});
|
});
|
||||||
const res = await server.get(`/share/${share.id}`);
|
const res = await server.get(`/s/${share.id}`);
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
expect(body).toContain("<title>Outline</title>");
|
expect(body).toContain("<title>Outline</title>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return standard title in html when share does not exist", async () => {
|
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();
|
const body = await res.text();
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
expect(body).toContain("<title>Outline</title>");
|
expect(body).toContain("<title>Outline</title>");
|
||||||
@@ -33,7 +33,7 @@ describe("/share/:id", () => {
|
|||||||
teamId: document.teamId,
|
teamId: document.teamId,
|
||||||
});
|
});
|
||||||
await document.destroy();
|
await document.destroy();
|
||||||
const res = await server.get(`/share/${share.id}`);
|
const res = await server.get(`/s/${share.id}`);
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
expect(body).toContain("<title>Outline</title>");
|
expect(body).toContain("<title>Outline</title>");
|
||||||
@@ -45,7 +45,7 @@ describe("/share/:id", () => {
|
|||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
teamId: document.teamId,
|
teamId: document.teamId,
|
||||||
});
|
});
|
||||||
const res = await server.get(`/share/${share.id}`);
|
const res = await server.get(`/s/${share.id}`);
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body).toContain(`<title>${document.title}</title>`);
|
expect(body).toContain(`<title>${document.title}</title>`);
|
||||||
@@ -57,7 +57,7 @@ describe("/share/:id", () => {
|
|||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
teamId: document.teamId,
|
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();
|
const body = await res.text();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body).toContain(`<title>${document.title}</title>`);
|
expect(body).toContain(`<title>${document.title}</title>`);
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ koa.use(
|
|||||||
|
|
||||||
koa.use<BaseContext, UserAgentContext>(userAgent);
|
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) {
|
if (isProduction) {
|
||||||
router.get("/static/*", async (ctx) => {
|
router.get("/static/*", async (ctx) => {
|
||||||
try {
|
try {
|
||||||
@@ -84,9 +92,9 @@ router.get("/opensearch.xml", (ctx) => {
|
|||||||
ctx.body = opensearchResponse(ctx.request.URL.origin);
|
ctx.body = opensearchResponse(ctx.request.URL.origin);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/share/:shareId", renderShare);
|
router.get("/s/:shareId", renderShare);
|
||||||
router.get("/share/:shareId/doc/:documentSlug", renderShare);
|
router.get("/s/:shareId/doc/:documentSlug", renderShare);
|
||||||
router.get("/share/:shareId/*", renderShare);
|
router.get("/s/:shareId/*", renderShare);
|
||||||
|
|
||||||
// catch all for application
|
// catch all for application
|
||||||
router.get("*", renderApp);
|
router.get("*", renderApp);
|
||||||
|
|||||||
Reference in New Issue
Block a user