Skip to content

Commit f9046d1

Browse files
committed
feat(devtools): enhance SERP preview with truncation logic and overflow reporting
This commit adds functionality to the SERP preview section, implementing text truncation for the title and description based on specified width limits. It introduces new state management for overflow detection and displays warnings when the title or description exceeds the defined character limits. Additionally, new styles are added to support the hidden measurement elements for accurate text sizing.
1 parent 6c45bce commit f9046d1

File tree

2 files changed

+100
-3
lines changed

2 files changed

+100
-3
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,23 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
283283
margin: 0;
284284
line-height: 1.5;
285285
`,
286+
serpMeasureHidden: css`
287+
position: absolute;
288+
left: -9999px;
289+
top: 0;
290+
white-space: nowrap;
291+
visibility: hidden;
292+
pointer-events: none;
293+
`,
294+
serpReportSection: css`
295+
margin-top: 1rem;
296+
font-size: 0.875rem;
297+
color: ${t(colors.gray[700], colors.gray[300])};
298+
`,
299+
serpReportItem: css`
300+
margin-top: 0.5rem;
301+
color: ${t(colors.yellow[700], colors.yellow[400])};
302+
`,
286303
devtoolsPanelContainer: (
287304
panelLocation: TanStackDevtoolsConfig['panelLocation'],
288305
isDetached: boolean,

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

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1-
import { createSignal } from 'solid-js'
1+
import { createEffect, createSignal } from 'solid-js'
22
import { useStyles } from '../../styles/use-styles'
33
import { useHeadChanges } from '../../hooks/use-head-changes'
44
import { Section, SectionDescription } from '@tanstack/devtools-ui'
55

6+
const TITLE_MAX_WIDTH_PX = 600
7+
const DESCRIPTION_MAX_WIDTH_PX = 960
8+
const ELLIPSIS = '...'
9+
10+
function truncateToWidth(
11+
el: HTMLDivElement,
12+
text: string,
13+
maxPx: number,
14+
): string {
15+
el.textContent = text
16+
if (el.offsetWidth <= maxPx) return text
17+
for (let i = text.length - 1; i >= 0; i--) {
18+
el.textContent = text.slice(0, i) + ELLIPSIS
19+
if (el.offsetWidth <= maxPx) return text.slice(0, i) + ELLIPSIS
20+
}
21+
return ELLIPSIS
22+
}
23+
624
type SerpData = {
725
title: string
826
description: string
@@ -50,12 +68,48 @@ function getSerpFromHead(): SerpData {
5068

5169
export function SerpPreviewSection() {
5270
const [serp, setSerp] = createSignal<SerpData>(getSerpFromHead())
71+
const [titleOverflow, setTitleOverflow] = createSignal(false)
72+
const [descriptionOverflow, setDescriptionOverflow] = createSignal(false)
73+
const [displayTitle, setDisplayTitle] = createSignal('')
74+
const [displayDescription, setDisplayDescription] = createSignal('')
75+
const [titleMeasureEl, setTitleMeasureEl] = createSignal<
76+
HTMLDivElement | undefined
77+
>(undefined)
78+
const [descMeasureEl, setDescMeasureEl] = createSignal<
79+
HTMLDivElement | undefined
80+
>(undefined)
5381
const styles = useStyles()
5482

5583
useHeadChanges(() => {
5684
setSerp(getSerpFromHead())
5785
})
5886

87+
createEffect(() => {
88+
const titleEl = titleMeasureEl()
89+
const descEl = descMeasureEl()
90+
const data = serp()
91+
if (!titleEl || !descEl) return
92+
93+
const titleText = data.title || 'No title'
94+
const descText = data.description || 'No meta description.'
95+
96+
const truncatedTitle = truncateToWidth(
97+
titleEl,
98+
titleText,
99+
TITLE_MAX_WIDTH_PX,
100+
)
101+
setDisplayTitle(truncatedTitle)
102+
setTitleOverflow(truncatedTitle !== titleText)
103+
104+
const truncatedDesc = truncateToWidth(
105+
descEl,
106+
descText,
107+
DESCRIPTION_MAX_WIDTH_PX,
108+
)
109+
setDisplayDescription(truncatedDesc)
110+
setDescriptionOverflow(truncatedDesc !== descText)
111+
})
112+
59113
const data = serp()
60114

61115
return (
@@ -81,12 +135,38 @@ export function SerpPreviewSection() {
81135
</div>
82136
</div>
83137
<div class={styles().serpSnippetTitle}>
84-
{data.title || 'No title'}
138+
{displayTitle() || data.title || 'No title'}
85139
</div>
140+
<div
141+
ref={setTitleMeasureEl}
142+
class={`${styles().serpSnippetTitle} ${styles().serpMeasureHidden}`}
143+
aria-hidden="true"
144+
/>
86145
<div class={styles().serpSnippetDesc}>
87-
{data.description || 'No meta description.'}
146+
{displayDescription() || data.description || 'No meta description.'}
88147
</div>
148+
<div
149+
ref={setDescMeasureEl}
150+
class={`${styles().serpSnippetDesc} ${styles().serpMeasureHidden}`}
151+
aria-hidden="true"
152+
/>
89153
</div>
154+
{(titleOverflow() || descriptionOverflow()) && (
155+
<div class={styles().serpReportSection}>
156+
{titleOverflow() && (
157+
<div class={styles().serpReportItem}>
158+
The title is wider than 600px and it may not be displayed in full
159+
length.
160+
</div>
161+
)}
162+
{descriptionOverflow() && (
163+
<div class={styles().serpReportItem}>
164+
The meta description may get trimmed at ~960 pixels on desktop
165+
and at ~680px on mobile. Keep it below ~158 characters.
166+
</div>
167+
)}
168+
</div>
169+
)}
90170
</Section>
91171
)
92172
}

0 commit comments

Comments
 (0)