Skip to content

Commit 2a06f34

Browse files
committed
feat(devtools): enhance SERP preview for mobile with new styles and overflow reporting
This commit introduces new styles for the mobile SERP preview, including a dedicated snippet layout and improved overflow reporting for the title and description. It adds functionality to handle mobile-specific text truncation and displays warnings for descriptions exceeding a three-line limit, enhancing user feedback and visual consistency across devices.
1 parent 7009617 commit 2a06f34

File tree

2 files changed

+151
-18
lines changed

2 files changed

+151
-18
lines changed

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
233233
`,
234234
serpPreviewBlock: css`
235235
margin-bottom: 1.5rem;
236+
border: 1px solid ${t(colors.gray[200], colors.gray[700])};
237+
border-radius: 10px;
238+
padding: 1rem;
236239
`,
237240
serpPreviewLabel: css`
238241
font-size: 0.875rem;
@@ -241,12 +244,32 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
241244
color: ${t(colors.gray[700], colors.gray[300])};
242245
`,
243246
serpSnippet: css`
244-
border: 1px solid ${t(colors.gray[200], colors.gray[800])};
247+
border: 1px solid ${t(colors.gray[100], colors.gray[800])};
245248
border-radius: 8px;
246249
padding: 1rem 1.25rem;
247250
background: ${t(colors.white, colors.darkGray[900])};
248251
max-width: 600px;
249252
font-family: ${fontFamily.sans};
253+
box-shadow: 0 1px 2px ${t('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.08)')};
254+
`,
255+
serpSnippetMobile: css`
256+
border: 1px solid ${t(colors.gray[100], colors.gray[800])};
257+
border-radius: 8px;
258+
padding: 1rem 1.25rem;
259+
background: ${t(colors.white, colors.darkGray[900])};
260+
max-width: 380px;
261+
font-family: ${fontFamily.sans};
262+
box-shadow: 0 1px 2px ${t('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.08)')};
263+
`,
264+
serpSnippetDescMobile: css`
265+
font-size: 0.875rem;
266+
color: ${t(colors.gray[700], colors.gray[300])};
267+
margin: 0;
268+
line-height: 1.5;
269+
display: -webkit-box;
270+
-webkit-box-orient: vertical;
271+
-webkit-line-clamp: 3;
272+
overflow: hidden;
250273
`,
251274
serpSnippetTopRow: css`
252275
display: flex;
@@ -316,6 +339,16 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
316339
visibility: hidden;
317340
pointer-events: none;
318341
`,
342+
serpMeasureHiddenMobile: css`
343+
position: absolute;
344+
left: -9999px;
345+
top: 0;
346+
width: 340px;
347+
visibility: hidden;
348+
pointer-events: none;
349+
font-size: 0.875rem;
350+
line-height: 1.5;
351+
`,
319352
serpReportSection: css`
320353
margin-top: 1rem;
321354
font-size: 0.875rem;

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

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Section, SectionDescription } from '@tanstack/devtools-ui'
55

66
const TITLE_MAX_WIDTH_PX = 600
77
const DESCRIPTION_MAX_WIDTH_PX = 960
8+
const DESCRIPTION_MOBILE_MAX_LINES = 3
89
const ELLIPSIS = '...'
910

1011
function truncateToWidth(
@@ -64,12 +65,10 @@ function getSerpFromHead(): SerpData {
6465
return { title, description, siteName, favicon, url }
6566
}
6667

67-
type SerpOverflow = {
68-
titleOverflow: boolean
69-
descriptionOverflow: boolean
70-
}
71-
72-
function getSerpReports(data: SerpData, overflow: SerpOverflow): string[] {
68+
function getSerpReportsDesktop(
69+
data: SerpData,
70+
overflow: { titleOverflow: boolean; descriptionOverflow: boolean },
71+
): string[] {
7372
const issues: string[] = []
7473
if (!data.title?.trim()) {
7574
issues.push('No title tag set on the page.')
@@ -93,10 +92,42 @@ function getSerpReports(data: SerpData, overflow: SerpOverflow): string[] {
9392
return issues
9493
}
9594

95+
function getSerpReportsMobile(
96+
data: SerpData,
97+
overflow: {
98+
titleOverflow: boolean
99+
descriptionOverflowMobile: boolean
100+
},
101+
): string[] {
102+
const issues: string[] = []
103+
if (!data.title?.trim()) {
104+
issues.push('No title tag set on the page.')
105+
}
106+
if (!data.description?.trim()) {
107+
issues.push('No meta description set on the page.')
108+
}
109+
if (!data.favicon) {
110+
issues.push('No favicon or icon set on the page.')
111+
}
112+
if (overflow.titleOverflow) {
113+
issues.push(
114+
'The title is wider than 600px and it may not be displayed in full length.',
115+
)
116+
}
117+
if (overflow.descriptionOverflowMobile) {
118+
issues.push(
119+
'Description exceeds the 3-line limit for mobile view. Please shorten your text to fit within 3 lines.',
120+
)
121+
}
122+
return issues
123+
}
124+
96125
export function SerpPreviewSection() {
97126
const [serp, setSerp] = createSignal<SerpData>(getSerpFromHead())
98127
const [titleOverflow, setTitleOverflow] = createSignal(false)
99128
const [descriptionOverflow, setDescriptionOverflow] = createSignal(false)
129+
const [descriptionOverflowMobile, setDescriptionOverflowMobile] =
130+
createSignal(false)
100131
const [displayTitle, setDisplayTitle] = createSignal('')
101132
const [displayDescription, setDisplayDescription] = createSignal('')
102133
const [titleMeasureEl, setTitleMeasureEl] = createSignal<
@@ -105,6 +136,9 @@ export function SerpPreviewSection() {
105136
const [descMeasureEl, setDescMeasureEl] = createSignal<
106137
HTMLDivElement | undefined
107138
>(undefined)
139+
const [descMeasureMobileEl, setDescMeasureMobileEl] = createSignal<
140+
HTMLDivElement | undefined
141+
>(undefined)
108142
const styles = useStyles()
109143

110144
useHeadChanges(() => {
@@ -114,6 +148,7 @@ export function SerpPreviewSection() {
114148
createEffect(() => {
115149
const titleEl = titleMeasureEl()
116150
const descEl = descMeasureEl()
151+
const descMobileEl = descMeasureMobileEl()
117152
const data = serp()
118153
if (!titleEl || !descEl) return
119154

@@ -135,15 +170,33 @@ export function SerpPreviewSection() {
135170
)
136171
setDisplayDescription(truncatedDesc)
137172
setDescriptionOverflow(truncatedDesc !== descText)
173+
174+
if (descMobileEl && descText) {
175+
descMobileEl.textContent = descText
176+
const lineHeight = parseFloat(
177+
getComputedStyle(descMobileEl).lineHeight,
178+
) || 20
179+
const lines = Math.ceil(descMobileEl.scrollHeight / lineHeight)
180+
setDescriptionOverflowMobile(lines > DESCRIPTION_MOBILE_MAX_LINES)
181+
} else {
182+
setDescriptionOverflowMobile(false)
183+
}
138184
})
139185

140-
const reports = createMemo(() =>
141-
getSerpReports(serp(), {
186+
const reportsDesktop = createMemo(() =>
187+
getSerpReportsDesktop(serp(), {
142188
titleOverflow: titleOverflow(),
143189
descriptionOverflow: descriptionOverflow(),
144190
}),
145191
)
146192

193+
const reportsMobile = createMemo(() =>
194+
getSerpReportsMobile(serp(), {
195+
titleOverflow: titleOverflow(),
196+
descriptionOverflowMobile: descriptionOverflowMobile(),
197+
}),
198+
)
199+
147200
const data = serp()
148201

149202
return (
@@ -189,17 +242,64 @@ export function SerpPreviewSection() {
189242
aria-hidden="true"
190243
/>
191244
</div>
245+
{reportsDesktop().length > 0 ? (
246+
<div class={styles().seoMissingTagsSection}>
247+
<strong>Missing issues for Desktop preview:</strong>
248+
<ul class={styles().serpErrorList}>
249+
<For each={reportsDesktop()}>
250+
{(issue) => (
251+
<li class={styles().serpReportItem}>{issue}</li>
252+
)}
253+
</For>
254+
</ul>
255+
</div>
256+
) : null}
192257
</div>
193-
{reports().length > 0 ? (
194-
<div class={styles().seoMissingTagsSection}>
195-
<strong>SERP preview issues:</strong>
196-
<ul class={styles().serpErrorList}>
197-
<For each={reports()}>
198-
{(issue) => <li class={styles().serpReportItem}>{issue}</li>}
199-
</For>
200-
</ul>
258+
<div class={styles().serpPreviewBlock}>
259+
<div class={styles().serpPreviewLabel}>Mobile preview</div>
260+
<div class={styles().serpSnippetMobile}>
261+
<div class={styles().serpSnippetTopRow}>
262+
{data.favicon ? (
263+
<img
264+
src={data.favicon}
265+
alt=""
266+
class={styles().serpSnippetFavicon}
267+
/>
268+
) : (
269+
<div class={styles().serpSnippetDefaultFavicon} />
270+
)}
271+
<div class={styles().serpSnippetSiteColumn}>
272+
<span class={styles().serpSnippetSiteName}>
273+
{data.siteName || data.url}
274+
</span>
275+
<span class={styles().serpSnippetSiteUrl}>{data.url}</span>
276+
</div>
277+
</div>
278+
<div class={styles().serpSnippetTitle}>
279+
{displayTitle() || data.title || 'No title'}
280+
</div>
281+
<div class={styles().serpSnippetDescMobile}>
282+
{displayDescription() || data.description || 'No meta description.'}
283+
</div>
284+
<div
285+
ref={setDescMeasureMobileEl}
286+
class={`${styles().serpSnippetDesc} ${styles().serpMeasureHiddenMobile}`}
287+
aria-hidden="true"
288+
/>
201289
</div>
202-
) : null}
290+
{reportsMobile().length > 0 ? (
291+
<div class={styles().seoMissingTagsSection}>
292+
<strong>Missing issues for Mobile preview:</strong>
293+
<ul class={styles().serpErrorList}>
294+
<For each={reportsMobile()}>
295+
{(issue) => (
296+
<li class={styles().serpReportItem}>{issue}</li>
297+
)}
298+
</For>
299+
</ul>
300+
</div>
301+
) : null}
302+
</div>
203303
</Section>
204304
)
205305
}

0 commit comments

Comments
 (0)