Skip to content

Commit b390f31

Browse files
authored
GraphQLConf 2025 — allow limited HTML in session descriptions (#2097)
* Allow limited HTML in session descriptions * Remove a console.log
1 parent c2ab6a9 commit b390f31

File tree

8 files changed

+231
-183
lines changed

8 files changed

+231
-183
lines changed

scripts/sync-sched/schedule-2025.json

Lines changed: 51 additions & 51 deletions
Large diffs are not rendered by default.

scripts/sync-sched/speakers.json

Lines changed: 99 additions & 85 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,45 @@
1-
import React from "react"
2-
31
const URL_REGEX = /https?:\/\/[^\s]+/g
2+
const LINK_REGEX = /<a\s+([^>]*href\s*=\s*[^>]*)>/gi
3+
4+
export function formatDescription(text: string): string {
5+
// we coerce all existing anchor tags to have target="_blank" rel="noopener noreferrer" and typography-link class
6+
const result = text.replace(LINK_REGEX, (_, attributes) => {
7+
let attrs = attributes
8+
9+
if (!attrs.includes("target=")) {
10+
attrs += ' target="_blank"'
11+
}
12+
13+
if (!attrs.includes("rel=")) {
14+
attrs += ' rel="noopener noreferrer"'
15+
}
16+
17+
if (!attrs.includes("class=")) {
18+
attrs += ' class="typography-link"'
19+
} else if (!attrs.includes("typography-link")) {
20+
attrs = attrs.replace(
21+
/class\s*=\s*["']([^"']*)/gi,
22+
'class="$1 typography-link',
23+
)
24+
}
25+
26+
return `<a ${attrs}>`
27+
})
428

5-
export function formatDescription(text: string): React.ReactNode {
6-
const res: React.ReactNode[] = []
29+
// then we format plain URLs that are not already inside an anchor tag
30+
return result.replace(URL_REGEX, (url, offset) => {
31+
const beforeUrl = result.slice(0, offset)
32+
const afterUrl = result.slice(offset + url.length)
733

8-
let lastIndex = 0
9-
let match: RegExpExecArray | null
34+
const lastOpenTag = beforeUrl.lastIndexOf("<")
35+
const lastCloseTag = beforeUrl.lastIndexOf(">")
36+
const nextCloseTag = afterUrl.indexOf(">")
1037

11-
while ((match = URL_REGEX.exec(text)) !== null) {
12-
if (match.index > lastIndex) {
13-
res.push(text.slice(lastIndex, match.index))
38+
if (lastOpenTag > lastCloseTag && nextCloseTag !== -1) {
39+
return url
1440
}
1541

16-
res.push(
17-
<a
18-
href={match[0]}
19-
target="_blank"
20-
rel="noopener noreferrer"
21-
className="typography-link"
22-
>
23-
{match[0].replace(/^https?:\/\//, "")}
24-
</a>,
25-
)
26-
27-
lastIndex = match.index + match[0].length
28-
}
29-
30-
if (lastIndex < text.length) {
31-
res.push(text.slice(lastIndex))
32-
}
33-
34-
return <>{res}</>
42+
const displayUrl = url.replace(/^https?:\/\//, "")
43+
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="typography-link">${displayUrl}</a>`
44+
})
3545
}

src/app/conf/2025/schedule/[id]/page.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,17 @@ export default function SessionPage({ params }: SessionProps) {
104104
</>
105105
)}
106106

107-
<h3 className="typography-h2 my-8 max-w-[408px] px-2 sm:px-3 lg:my-16">
108-
Session speakers
109-
</h3>
110-
<SessionSpeakers
111-
session={session}
112-
className="-mx-px -mb-px last:xl:pb-24"
113-
/>
107+
{!!session.speakers?.length && (
108+
<>
109+
<h3 className="typography-h2 my-8 max-w-[408px] px-2 sm:px-3 lg:my-16">
110+
Session speakers
111+
</h3>
112+
<SessionSpeakers
113+
session={session}
114+
className="-mx-px -mb-px last:xl:pb-24"
115+
/>
116+
</>
117+
)}
114118

115119
{!!session.files?.length && (
116120
<>
@@ -255,9 +259,13 @@ function SessionDescription({ session }: { session: ScheduleSession }) {
255259
return (
256260
<div className="mt-8 flex gap-4 px-2 pb-8 max-lg:flex-col sm:px-3 lg:mt-16 lg:gap-8 xl:pb-16">
257261
<h3 className="typography-h2 min-w-[320px]">Session description</h3>
258-
<p className="typography-body-lg whitespace-pre-wrap">
259-
{formattedDescription}
260-
</p>
262+
<p
263+
className="typography-body-lg whitespace-pre-wrap"
264+
dangerouslySetInnerHTML={{
265+
// the description was partially sanitized when syncinc data from sched
266+
__html: formattedDescription,
267+
}}
268+
/>
261269
</div>
262270
)
263271
}

src/app/conf/2025/schedule/_components/schedule-session-card.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
MenuItems,
99
Transition,
1010
} from "@headlessui/react"
11+
import { stripHtml } from "string-strip-html"
1112

1213
import { SchedSpeaker, ScheduleSession } from "@/app/conf/_api/sched-types"
1314
import { Anchor } from "@/app/conf/_design-system/anchor"
@@ -132,10 +133,12 @@ export function ScheduleSessionCard({
132133
</span>
133134
)}
134135
<span className="mt-4 flex items-center gap-2 xl:mt-6">
135-
<span className="typography-body-xs flex items-center gap-0.5">
136-
<PinIcon className="size-4 text-pri-base [@container(width<240px)]:hidden" />
137-
{session.venue}
138-
</span>
136+
{session.venue && (
137+
<span className="typography-body-xs flex items-center gap-0.5">
138+
<PinIcon className="size-4 text-pri-base [@container(width<240px)]:hidden" />
139+
{session.venue}
140+
</span>
141+
)}
139142
{blockTimeFraction < 1 && (
140143
<span className="typography-body-xs flex items-center gap-0.5">
141144
<ClockIcon className="size-4 text-pri-base [@container(width<240px)]:hidden" />
@@ -176,7 +179,7 @@ function AddToCalendarLink({
176179
title: eventTitle,
177180
start: session.event_start,
178181
end: session.event_end,
179-
description: session.description,
182+
description: stripHtml(session.description).result,
180183
location: session.venue,
181184
organizer: {
182185
name: `GraphQLConf ${new Date().getFullYear()}`,

src/app/conf/2025/speakers/[id]/long-session-card.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import React, { Fragment } from "react"
1515
import { SessionTags } from "../../components/session-tags"
1616
import { Menu, MenuItem, MenuItems, Transition } from "@headlessui/react"
1717
import { MenuButton } from "@headlessui/react"
18+
import { stripHtml } from "string-strip-html"
1819

1920
export interface LongSessionCardProps
2021
extends React.HTMLAttributes<HTMLDivElement> {
@@ -172,7 +173,7 @@ function AddToCalendarLink({
172173
title: eventTitle,
173174
start: session.event_start,
174175
end: session.event_end,
175-
description: session.description,
176+
description: stripHtml(session.description).result,
176177
location: session.venue,
177178
organizer: {
178179
name: `GraphQLConf ${new Date().getFullYear()}`,

src/app/conf/_api/sched-client.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ export async function getSchedule(
135135
...session,
136136
event_type,
137137
event_subtype,
138-
description: preprocessDescription(description),
138+
description: preprocessDescription(description, {
139+
allowSomeHtml: true,
140+
}),
139141
}
140142
})
141143

@@ -183,15 +185,22 @@ export async function getSpeakerDetails(
183185
return shapeSpeaker(data as SchedSpeaker)
184186
}
185187

186-
function preprocessDescription(description: string | undefined | null): string {
188+
function preprocessDescription(
189+
description: string | undefined | null,
190+
options: { allowSomeHtml?: boolean } = {},
191+
): string {
187192
let res = description || ""
188193

189194
// we respect manual line breaks
190195
res = res.replace(/<br\s*\/?>/g, "\n")
191196

192197
// respecting <li> and <a> tags doesn't make sense, because speakers don't use them consistently
193198
// we'll improve how the descriptions look later down the tree in the session details page
194-
return stripHtml(res).result
199+
return stripHtml(res, {
200+
ignoreTags: options.allowSomeHtml
201+
? ["a", "b", "i", "em", "strong", "code", "pre", "ul", "ol", "li"]
202+
: [],
203+
}).result
195204
}
196205

197206
function shapeSpeaker(user: SchedSpeaker): SchedSpeaker {

src/app/conf/_api/sched-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ export type ScheduleSession = {
22
id: string
33
active: "Y" | "N"
44
audience: string
5+
/**
6+
* can include HTML tags
7+
*/
58
description: string
69
event_end: string
710
event_start: string

0 commit comments

Comments
 (0)