Skip to content

Commit cfa0fbb

Browse files
committed
refactor(devtools): update SERP preview truncation logic and improve character limits
This commit refines the SERP preview component by replacing pixel-based truncation with character-based limits for titles and descriptions. It introduces constants for maximum character counts, enhancing the clarity of the truncation logic. Additionally, it simplifies the component structure by removing unnecessary measurement elements, improving overall readability and maintainability.
1 parent 01712ad commit cfa0fbb

File tree

2 files changed

+27
-94
lines changed

2 files changed

+27
-94
lines changed

packages/devtools/src/styles/use-styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,9 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
335335
position: absolute;
336336
left: -9999px;
337337
top: 0;
338-
white-space: nowrap;
339338
visibility: hidden;
340339
pointer-events: none;
340+
box-sizing: border-box;
341341
`,
342342
serpMeasureHiddenMobile: css`
343343
position: absolute;

packages/devtools/src/tabs/seo-tab/serp-preview.tsx

Lines changed: 26 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { For, createMemo, createSignal } from 'solid-js'
33
import { useHeadChanges } from '../../hooks/use-head-changes'
44
import { useStyles } from '../../styles/use-styles'
55

6-
const TITLE_MAX_WIDTH_PX = 600
7-
const DESCRIPTION_MAX_WIDTH_PX = 960
8-
const DESCRIPTION_MOBILE_MAX_LINES = 3
6+
/** Google typically truncates titles at ~60 characters. */
7+
const TITLE_MAX_CHARS = 60
8+
/** Meta description is often trimmed at ~158 characters on desktop. */
9+
const DESCRIPTION_MAX_CHARS = 158
10+
/** Approximate characters that fit in 3 lines at mobile width (~340px, ~14px font). */
11+
const DESCRIPTION_MOBILE_MAX_CHARS = 120
912
const ELLIPSIS = '...'
1013

1114
type SerpData = {
@@ -78,18 +81,10 @@ const SERP_PREVIEWS: Array<SerpPreview> = [
7881
},
7982
]
8083

81-
function truncateToWidth(
82-
el: HTMLDivElement,
83-
text: string,
84-
maxPx: number,
85-
): string {
86-
el.textContent = text
87-
if (el.offsetWidth <= maxPx) return text
88-
for (let i = text.length - 1; i >= 0; i--) {
89-
el.textContent = text.slice(0, i) + ELLIPSIS
90-
if (el.offsetWidth <= maxPx) return text.slice(0, i) + ELLIPSIS
91-
}
92-
return ELLIPSIS
84+
function truncateToChars(text: string, maxChars: number): string {
85+
if (text.length <= maxChars) return text
86+
if (maxChars <= ELLIPSIS.length) return ELLIPSIS
87+
return text.slice(0, maxChars - ELLIPSIS.length) + ELLIPSIS
9388
}
9489

9590
function getSerpFromHead(): SerpData {
@@ -142,9 +137,6 @@ function SerpSnippetPreview(props: {
142137
isMobile: boolean
143138
label: string
144139
issues: Array<string>
145-
setTitleMeasureEl: (el: HTMLDivElement) => void
146-
setDescMeasureEl: (el: HTMLDivElement) => void
147-
setDescMeasureMobileEl: (el: HTMLDivElement) => void
148140
}) {
149141
const styles = useStyles()
150142

@@ -177,37 +169,18 @@ function SerpSnippetPreview(props: {
177169
{props.displayTitle || props.data.title || 'No title'}
178170
</div>
179171
{!props.isMobile && (
180-
<>
181-
<div
182-
ref={props.setTitleMeasureEl}
183-
class={`${styles().serpSnippetTitle} ${styles().serpMeasureHidden}`}
184-
aria-hidden="true"
185-
/>
186-
<div class={styles().serpSnippetDesc}>
187-
{props.displayDescription ||
188-
props.data.description ||
189-
'No meta description.'}
190-
</div>
191-
<div
192-
ref={props.setDescMeasureEl}
193-
class={`${styles().serpSnippetDesc} ${styles().serpMeasureHidden}`}
194-
aria-hidden="true"
195-
/>
196-
</>
172+
<div class={styles().serpSnippetDesc}>
173+
{props.displayDescription ||
174+
props.data.description ||
175+
'No meta description.'}
176+
</div>
197177
)}
198178
{props.isMobile && (
199-
<>
200-
<div class={styles().serpSnippetDescMobile}>
201-
{props.displayDescription ||
202-
props.data.description ||
203-
'No meta description.'}
204-
</div>
205-
<div
206-
ref={props.setDescMeasureMobileEl}
207-
class={`${styles().serpSnippetDesc} ${styles().serpMeasureHiddenMobile}`}
208-
aria-hidden="true"
209-
/>
210-
</>
179+
<div class={styles().serpSnippetDescMobile}>
180+
{props.displayDescription ||
181+
props.data.description ||
182+
'No meta description.'}
183+
</div>
211184
)}
212185
</div>
213186
{props.issues.length > 0 ? (
@@ -226,64 +199,27 @@ function SerpSnippetPreview(props: {
226199

227200
export function SerpPreviewSection() {
228201
const [serp, setSerp] = createSignal<SerpData>(getSerpFromHead())
229-
const [titleMeasureEl, setTitleMeasureEl] = createSignal<
230-
HTMLDivElement | undefined
231-
>(undefined)
232-
const [descMeasureEl, setDescMeasureEl] = createSignal<
233-
HTMLDivElement | undefined
234-
>(undefined)
235-
const [descMeasureMobileEl, setDescMeasureMobileEl] = createSignal<
236-
HTMLDivElement | undefined
237-
>(undefined)
238202

239203
useHeadChanges(() => {
240204
setSerp(getSerpFromHead())
241205
})
242206

243207
const serpPreviewState = createMemo(() => {
244-
const titleEl = titleMeasureEl()
245-
const descEl = descMeasureEl()
246-
const descMobileEl = descMeasureMobileEl()
247208
const data = serp()
248209
const titleText = data.title || 'No title'
249210
const descText = data.description || 'No meta description.'
250211

251-
if (!titleEl || !descEl) {
252-
return {
253-
displayTitle: titleText,
254-
displayDescription: descText,
255-
overflow: {
256-
titleOverflow: false,
257-
descriptionOverflow: false,
258-
descriptionOverflowMobile: false,
259-
},
260-
}
261-
}
262-
263-
const displayTitle = truncateToWidth(titleEl, titleText, TITLE_MAX_WIDTH_PX)
264-
const displayDescription = truncateToWidth(
265-
descEl,
266-
descText,
267-
DESCRIPTION_MAX_WIDTH_PX,
268-
)
269-
270-
let descriptionOverflowMobile = false
271-
272-
if (descMobileEl && descText) {
273-
descMobileEl.textContent = descText
274-
const lineHeight =
275-
parseFloat(getComputedStyle(descMobileEl).lineHeight) || 20
276-
const lines = Math.ceil(descMobileEl.scrollHeight / lineHeight)
277-
descriptionOverflowMobile = lines > DESCRIPTION_MOBILE_MAX_LINES
278-
}
212+
const displayTitle = truncateToChars(titleText, TITLE_MAX_CHARS)
213+
const displayDescription = truncateToChars(descText, DESCRIPTION_MAX_CHARS)
279214

280215
return {
281216
displayTitle,
282217
displayDescription,
283218
overflow: {
284-
titleOverflow: displayTitle !== titleText,
285-
descriptionOverflow: displayDescription !== descText,
286-
descriptionOverflowMobile,
219+
titleOverflow: titleText.length > TITLE_MAX_CHARS,
220+
descriptionOverflow: descText.length > DESCRIPTION_MAX_CHARS,
221+
descriptionOverflowMobile:
222+
descText.length > DESCRIPTION_MOBILE_MAX_CHARS,
287223
},
288224
}
289225
})
@@ -311,9 +247,6 @@ export function SerpPreviewSection() {
311247
isMobile={preview.isMobile}
312248
label={preview.label}
313249
issues={issues()}
314-
setTitleMeasureEl={setTitleMeasureEl}
315-
setDescMeasureEl={setDescMeasureEl}
316-
setDescMeasureMobileEl={setDescMeasureMobileEl}
317250
/>
318251
)
319252
}}

0 commit comments

Comments
 (0)