feat: Allow / disallow SEO indexing on booking pages (#10566)

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Anshuman Pandey 2023-08-15 06:14:09 +05:30 committed by GitHub
parent e148ee7823
commit b0d1720d92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 88 additions and 42 deletions

View File

@ -36,6 +36,7 @@ import { ssrInit } from "@server/lib/ssr";
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(profile.theme);
const { t } = useLocale();
@ -82,6 +83,10 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
profile: { name: `${profile.name}`, image: null },
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
nextSeoProps={{
noindex: !profile.allowSEOIndexing,
nofollow: !profile.allowSEOIndexing,
}}
/>
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
@ -214,6 +219,7 @@ export type UserPageProps = {
theme: string | null;
brandColor: string;
darkBrandColor: string;
allowSEOIndexing: boolean;
};
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
themeBasis: string | null;
@ -276,6 +282,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
away: true,
verified: true,
allowDynamicBooking: true,
allowSEOIndexing: true,
},
});
@ -315,6 +322,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
allowSEOIndexing: user.allowSEOIndexing ?? true,
};
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);

View File

@ -30,6 +30,7 @@ export default function Type({
booking,
away,
isBrandingHidden,
isSEOIndexable,
rescheduleUid,
entity,
duration,
@ -41,6 +42,7 @@ export default function Type({
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
isSEOIndexable={isSEOIndexable ?? true}
entity={entity}
/>
<Booker
@ -128,6 +130,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
isSEOIndexable: true,
themeBasis: null,
bookingUid: bookingUid ? `${bookingUid}` : null,
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
@ -154,6 +157,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
select: {
away: true,
hideBranding: true,
allowSEOIndexing: true,
},
});
@ -199,6 +203,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
entity: eventData.entity,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
isSEOIndexable: user?.allowSEOIndexing,
themeBasis: username,
bookingUid: bookingUid ? `${bookingUid}` : null,
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,

View File

@ -116,6 +116,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1),
},
allowDynamicBooking: user.allowDynamicBooking ?? true,
allowSEOIndexing: user.allowSEOIndexing ?? true,
},
});
const {
@ -124,6 +125,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
getValues,
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
return (
<Form
form={formMethods}
@ -225,6 +227,24 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
)}
/>
</div>
<div className="mt-8">
<Controller
name="allowSEOIndexing"
control={formMethods.control}
render={() => (
<SettingsToggle
title={t("seo_indexing")}
description={t("allow_seo_indexing")}
checked={formMethods.getValues("allowSEOIndexing")}
onCheckedChange={(checked) => {
formMethods.setValue("allowSEOIndexing", checked, { shouldDirty: true });
}}
/>
)}
/>
</div>
<Button
loading={mutation.isLoading}
disabled={isDisabled}

View File

@ -407,6 +407,8 @@
"allow_dynamic_booking_tooltip": "Group booking links that can be created dynamically by adding multiple usernames with a '+'. example: '{{appName}}/bailey+peer'",
"allow_dynamic_booking": "Allow attendees to book you through dynamic group bookings",
"dynamic_booking": "Dynamic group links",
"allow_seo_indexing": "Allow search engines to access your public content",
"seo_indexing": "Allow SEO Indexing",
"email": "Email",
"email_placeholder": "jdoe@example.com",
"full_name": "Full name",

View File

@ -7,6 +7,7 @@ interface BookerSeoProps {
eventSlug: string;
rescheduleUid: string | undefined;
hideBranding?: boolean;
isSEOIndexable?: boolean;
isTeamEvent?: boolean;
entity: {
orgSlug?: string | null;
@ -16,7 +17,7 @@ interface BookerSeoProps {
}
export const BookerSeo = (props: BookerSeoProps) => {
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, entity } = props;
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, entity, isSEOIndexable } = props;
const { t } = useLocale();
const { data: event } = trpc.viewer.public.event.useQuery(
{ username, eventSlug, isTeamEvent, org: entity.orgSlug ?? null },
@ -41,8 +42,8 @@ export const BookerSeo = (props: BookerSeoProps) => {
],
}}
nextSeoProps={{
nofollow: event?.hidden,
noindex: event?.hidden,
nofollow: event?.hidden || !isSEOIndexable,
noindex: event?.hidden || !isSEOIndexable,
}}
isBrandingHidden={hideBranding}
/>

View File

@ -220,6 +220,7 @@ export const buildUser = <T extends Partial<UserPayload>>(user?: T): UserPayload
verified: false,
weekStart: "",
organizationId: null,
allowSEOIndexing: null,
...user,
};
};

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "allowSEOIndexing" BOOLEAN DEFAULT true;

View File

@ -168,50 +168,54 @@ enum UserPermissionRole {
}
model User {
id Int @id @default(autoincrement())
username String?
name String?
id Int @id @default(autoincrement())
username String?
name String?
/// @zod.email()
email String
emailVerified DateTime?
password String?
bio String?
avatar String?
timeZone String @default("Europe/London")
weekStart String @default("Sunday")
email String
emailVerified DateTime?
password String?
bio String?
avatar String?
timeZone String @default("Europe/London")
weekStart String @default("Sunday")
// DEPRECATED - TO BE REMOVED
startTime Int @default(0)
endTime Int @default(1440)
startTime Int @default(0)
endTime Int @default(1440)
// </DEPRECATED>
bufferTime Int @default(0)
hideBranding Boolean @default(false)
theme String?
createdDate DateTime @default(now()) @map(name: "created")
trialEndsAt DateTime?
eventTypes EventType[] @relation("user_eventtype")
credentials Credential[]
teams Membership[]
bookings Booking[]
schedules Schedule[]
defaultScheduleId Int?
selectedCalendars SelectedCalendar[]
completedOnboarding Boolean @default(false)
locale String?
timeFormat Int? @default(12)
twoFactorSecret String?
twoFactorEnabled Boolean @default(false)
identityProvider IdentityProvider @default(CAL)
identityProviderId String?
availability Availability[]
invitedTo Int?
webhooks Webhook[]
brandColor String @default("#292929")
darkBrandColor String @default("#fafafa")
bufferTime Int @default(0)
hideBranding Boolean @default(false)
theme String?
createdDate DateTime @default(now()) @map(name: "created")
trialEndsAt DateTime?
eventTypes EventType[] @relation("user_eventtype")
credentials Credential[]
teams Membership[]
bookings Booking[]
schedules Schedule[]
defaultScheduleId Int?
selectedCalendars SelectedCalendar[]
completedOnboarding Boolean @default(false)
locale String?
timeFormat Int? @default(12)
twoFactorSecret String?
twoFactorEnabled Boolean @default(false)
identityProvider IdentityProvider @default(CAL)
identityProviderId String?
availability Availability[]
invitedTo Int?
webhooks Webhook[]
brandColor String @default("#292929")
darkBrandColor String @default("#fafafa")
// the location where the events will end up
destinationCalendar DestinationCalendar?
away Boolean @default(false)
destinationCalendar DestinationCalendar?
away Boolean @default(false)
// participate in dynamic group booking or not
allowDynamicBooking Boolean? @default(true)
allowDynamicBooking Boolean? @default(true)
// participate in SEO indexing or not
allowSEOIndexing Boolean? @default(true)
/// @zod.custom(imports.userMetadata)
metadata Json?
verified Boolean? @default(false)

View File

@ -57,6 +57,7 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
role: true,
organizationId: true,
allowDynamicBooking: true,
allowSEOIndexing: true,
organization: {
select: {
id: true,

View File

@ -42,6 +42,7 @@ export const meHandler = async ({ ctx }: MeOptions) => {
metadata: user.metadata,
defaultBookerLayouts: user.defaultBookerLayouts,
allowDynamicBooking: user.allowDynamicBooking,
allowSEOIndexing: user.allowSEOIndexing,
organizationId: user.organizationId,
organization: user.organization,
};

View File

@ -13,6 +13,7 @@ export const ZUpdateProfileInputSchema = z.object({
weekStart: z.string().optional(),
hideBranding: z.boolean().optional(),
allowDynamicBooking: z.boolean().optional(),
allowSEOIndexing: z.boolean().optional(),
brandColor: z.string().optional(),
darkBrandColor: z.string().optional(),
theme: z.string().optional().nullable(),