diff --git a/app/components/Sharing/Document/PublicAccess.tsx b/app/components/Sharing/Document/PublicAccess.tsx index 38353e7099..d2fc2bd90e 100644 --- a/app/components/Sharing/Document/PublicAccess.tsx +++ b/app/components/Sharing/Document/PublicAccess.tsx @@ -1,12 +1,13 @@ import debounce from "lodash/debounce"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; -import { CopyIcon, GlobeIcon, InfoIcon } from "outline-icons"; +import { CopyIcon, GlobeIcon, InfoIcon, QuestionMarkIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { toast } from "sonner"; import styled, { useTheme } from "styled-components"; +import Flex from "@shared/components/Flex"; import Squircle from "@shared/components/Squircle"; import { s } from "@shared/styles"; import { UrlHelper } from "@shared/utils/UrlHelper"; @@ -50,6 +51,19 @@ function PublicAccess({ document, share, sharedParent }: Props) { setUrlId(share?.urlId); }, [share?.urlId]); + const handleIndexingChanged = React.useCallback( + async (event) => { + try { + await share?.save({ + allowIndexing: event.currentTarget.checked, + }); + } catch (err) { + toast.error(err.message); + } + }, + [share] + ); + const handlePublishedChange = React.useCallback( async (event) => { try { @@ -175,6 +189,32 @@ function PublicAccess({ document, share, sharedParent }: Props) { ) : null} + {share?.published && ( + + {t("Search engine indexing")}  + + + + + } + actions={ + + } + /> + )} + {share?.published && !share.includeChildDocuments ? ( diff --git a/app/models/Share.ts b/app/models/Share.ts index 6a1e945c73..06d3c9740e 100644 --- a/app/models/Share.ts +++ b/app/models/Share.ts @@ -55,6 +55,10 @@ class Share extends Model { @observable url: string; + @Field + @observable + allowIndexing: boolean; + /** The user that shared the document. */ @Relation(() => User, { onDelete: "null" }) createdBy: User; diff --git a/server/migrations/20241105132600-add-allowIndexing-to-shares.js b/server/migrations/20241105132600-add-allowIndexing-to-shares.js new file mode 100644 index 0000000000..74065e5143 --- /dev/null +++ b/server/migrations/20241105132600-add-allowIndexing-to-shares.js @@ -0,0 +1,15 @@ +"use strict"; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("shares", "allowIndexing", { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true, + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn("shares", "allowIndexing"); + }, +}; diff --git a/server/models/Share.ts b/server/models/Share.ts index 91b217681e..c039ccdc62 100644 --- a/server/models/Share.ts +++ b/server/models/Share.ts @@ -184,6 +184,10 @@ class Share extends IdModel< @Column(DataType.UUID) documentId: string; + @Default(true) + @Column + allowIndexing: boolean; + revoke(userId: string) { this.revokedAt = new Date(); this.revokedById = userId; diff --git a/server/presenters/share.ts b/server/presenters/share.ts index 58c9e8010c..14acf3ed4a 100644 --- a/server/presenters/share.ts +++ b/server/presenters/share.ts @@ -12,6 +12,7 @@ export default function presentShare(share: Share, isAdmin = false) { urlId: share.urlId, createdBy: presentUser(share.user), includeChildDocuments: share.includeChildDocuments, + allowIndexing: share.allowIndexing, lastAccessedAt: share.lastAccessedAt || undefined, views: share.views || 0, domain: share.domain, diff --git a/server/routes/api/shares/schema.ts b/server/routes/api/shares/schema.ts index 6a263f5edd..4c5f469488 100644 --- a/server/routes/api/shares/schema.ts +++ b/server/routes/api/shares/schema.ts @@ -51,6 +51,7 @@ export const SharesUpdateSchema = BaseSchema.extend({ id: z.string().uuid(), includeChildDocuments: z.boolean().optional(), published: z.boolean().optional(), + allowIndexing: z.boolean().optional(), urlId: z .string() .regex(UrlHelper.SHARE_URL_SLUG_REGEX, { diff --git a/server/routes/api/shares/shares.ts b/server/routes/api/shares/shares.ts index d5b2dcd257..565aa5a464 100644 --- a/server/routes/api/shares/shares.ts +++ b/server/routes/api/shares/shares.ts @@ -230,7 +230,8 @@ router.post( auth(), validate(T.SharesUpdateSchema), async (ctx: APIContext) => { - const { id, includeChildDocuments, published, urlId } = ctx.input.body; + const { id, includeChildDocuments, published, urlId, allowIndexing } = + ctx.input.body; const { user } = ctx.state.auth; authorize(user, "share", user.team); @@ -257,6 +258,10 @@ router.post( share.urlId = urlId; } + if (allowIndexing !== undefined) { + share.allowIndexing = allowIndexing; + } + await share.save(); await Event.createFromContext(ctx, { name: "shares.update", diff --git a/server/routes/app.ts b/server/routes/app.ts index d4286c76db..aad80bb4c6 100644 --- a/server/routes/app.ts +++ b/server/routes/app.ts @@ -54,6 +54,7 @@ export const renderApp = async ( rootShareId?: string; isShare?: boolean; analytics?: Integration[]; + allowIndexing?: boolean; } = {} ) => { const { @@ -61,6 +62,7 @@ export const renderApp = async ( description = "A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, & more…", canonical = "", shortcutIcon = `${env.CDN_URL || ""}/images/favicon-32.png`, + allowIndexing = true, } = options; if (ctx.request.path === "/realtime/") { @@ -91,6 +93,10 @@ export const renderApp = async ( `; + const noIndexTag = allowIndexing + ? "" + : ''; + const scriptTags = env.isProduction ? `