Skip to content

Commit 82e77d6

Browse files
committed
Fix mobile search accessibility and upgrade search results design to modern card layout
1 parent d72a9bc commit 82e77d6

3 files changed

Lines changed: 73 additions & 13 deletions

File tree

‎public/pagefind-search.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,17 @@
5353
? items
5454
.map(function (item) {
5555
var title = escapeHtml((item.meta && item.meta.title) || '');
56-
var excerpt = item.excerpt ? '<span>' + escapeHtml(cleanExcerpt(item.excerpt, (item.meta && item.meta.title) || '')) + '</span>' : '';
57-
return '<a class="result" href="' + item.url + '"><strong>' + title + '</strong>' + excerpt + '</a>';
56+
var category = escapeHtml((item.meta && item.meta.category) || 'ARTICLE');
57+
var excerpt = item.excerpt ? '<p class="result-excerpt">' + escapeHtml(cleanExcerpt(item.excerpt, (item.meta && item.meta.title) || '')) + '</p>' : '';
58+
return (
59+
'<a class="result-card" href="' + item.url + '">' +
60+
'<div class="result-content">' +
61+
'<h3 class="result-title">' + title + '</h3>' +
62+
excerpt +
63+
'</div>' +
64+
'<span class="result-badge">' + category + '</span>' +
65+
'</a>'
66+
);
5867
})
5968
.join('')
6069
: '<p class="search-empty">No matching articles yet. Try a broader term.</p>';

‎src/components/search/SearchBox.astro‎

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,68 @@ const statusId = `${id}-search-status`;
1616
<div id={resultsId} class="search-results"></div>
1717
</div>
1818
<style>
19-
.search-box { display: grid; gap: .35rem; width: 100%; }
20-
.search-box input { width: 100%; padding: .75rem 1rem; border: 1px solid var(--line); border-radius: .65rem; background: var(--surface); color: var(--text); font-size: .95rem; }
21-
.search-box input:focus { outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent); outline-offset: 0; border-color: transparent; }
22-
.search-status { min-height: 1.1rem; margin: 0; color: var(--muted); font-size: .8rem; }
19+
.search-box { display: grid; gap: .75rem; width: 100%; }
20+
.search-box input { width: 100%; padding: .75rem 1rem; border: 1px solid var(--line); border-radius: .75rem; background: var(--surface); color: var(--text); font-size: .95rem; transition: border-color .15s ease, box-shadow .15s ease; }
21+
.search-box input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }
22+
.search-status { min-height: 1.1rem; margin: 0; color: var(--muted); font-size: .82rem; font-weight: 500; }
2323
.search-status:empty { display: none; }
24-
.search-results { display: grid; gap: .4rem; max-height: 22rem; overflow: auto; }
24+
.search-results { display: grid; gap: .65rem; max-height: 26rem; overflow-y: auto; padding-right: .2rem; }
2525
.search-results:empty { display: none; }
26-
.result { display: grid; gap: .15rem; padding: .7rem .8rem; border-radius: .55rem; background: var(--surface-raised); text-decoration: none; }
27-
.result:hover { outline: 1px solid var(--line); }
28-
.result strong { color: var(--text); font-size: .92rem; }
29-
.result span { color: var(--muted); font-size: .82rem; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
30-
.search-empty { margin: 0; padding: .6rem .2rem; color: var(--muted); font-size: .9rem; }
26+
27+
:global(.result-card) {
28+
display: flex;
29+
flex-direction: column;
30+
align-items: flex-start;
31+
gap: .6rem;
32+
padding: 1rem 1.1rem;
33+
border: 1px solid var(--line);
34+
border-radius: .85rem;
35+
background: var(--surface-raised);
36+
text-decoration: none;
37+
transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
38+
}
39+
:global(.result-card:hover) {
40+
border-color: color-mix(in srgb, var(--accent) 60%, var(--line));
41+
box-shadow: 0 .4rem 1.2rem color-mix(in srgb, var(--line) 40%, transparent);
42+
transform: translateY(-1px);
43+
}
44+
:global(.result-content) {
45+
display: grid;
46+
gap: .3rem;
47+
width: 100%;
48+
}
49+
:global(.result-title) {
50+
margin: 0;
51+
color: var(--text);
52+
font-family: var(--font-display);
53+
font-size: .98rem;
54+
font-weight: 700;
55+
line-height: 1.35;
56+
}
57+
:global(.result-excerpt) {
58+
margin: 0;
59+
color: var(--muted);
60+
font-size: .83rem;
61+
line-height: 1.45;
62+
display: -webkit-box;
63+
-webkit-line-clamp: 2;
64+
-webkit-box-orient: vertical;
65+
overflow: hidden;
66+
}
67+
:global(.result-badge) {
68+
display: inline-flex;
69+
align-items: center;
70+
padding: .25rem .65rem;
71+
border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
72+
border-radius: 999px;
73+
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
74+
color: var(--accent);
75+
font-size: .7rem;
76+
font-weight: 800;
77+
letter-spacing: .06em;
78+
text-transform: uppercase;
79+
line-height: 1;
80+
}
81+
.search-empty { margin: 0; padding: .8rem .2rem; color: var(--muted); font-size: .9rem; }
3182
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); }
3283
</style>

‎src/pages/articles/[slug].astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ const related = relatedPosts(summary, summaries);
2828
const coverSrc = typeof summary.cover.image === 'string' ? summary.cover.image : summary.cover.image.src;
2929
const schema = { '@context': 'https://schema.org', '@type': 'BlogPosting', headline: summary.title, description: summary.description, datePublished: summary.publishedAt.toISOString(), dateModified: (summary.updatedAt ?? summary.publishedAt).toISOString(), mainEntityOfPage: new URL(summary.url, Astro.site), author: { '@type': 'Organization', name: summary.author.name }, publisher: { '@type': 'Organization', name: 'NodeHunt' }, image: new URL(coverSrc, Astro.site) };
3030
---
31-
<BaseLayout title={post.data.seo.title ?? summary.title} description={post.data.seo.description ?? summary.description} noindex={post.data.seo.noindex} jsonLd={schema}><article class="container article" data-pagefind-body><nav class="crumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href={summary.category.url}>{summary.category.name}</a><span>/</span><span aria-current="page">{summary.title}</span></nav><header><p class="eyebrow">{summary.category.name}</p><h1>{summary.title}</h1><p class="dek">{summary.description}</p><div class="byline" data-pagefind-ignore><AuthorByline author={summary.author} /><span>·</span><time datetime={summary.publishedAt.toISOString()}>{formatDate(summary.publishedAt)}</time>{summary.updatedAt && <><span>·</span><span>Updated {formatDate(summary.updatedAt)}</span></>}<span>·</span><span>{summary.readingTime} min read</span></div></header><figure><Image src={summary.cover.image} alt={summary.cover.alt} widths={[800, 1200, 1600]} sizes="(max-width: 800px) 100vw, 800px" loading="eager" fetchpriority="high" inferSize />{summary.cover.caption && <figcaption>{summary.cover.caption}</figcaption>}</figure>{headings.length >= 3 && <TableOfContents {headings} />}<div class="prose"><Content components={{ Callout, ProsCons, ComparisonTable, FAQ }} /></div><div id="mid-article-ad"><MidArticleAd /></div><script>const host = document.getElementById('mid-article-ad');const prose = host && host.closest('.article') && host.closest('.article').querySelector('.prose');if (host && prose) { const headings = prose.querySelectorAll('h2'); if (headings.length >= 3) { headings[Math.floor(headings.length / 2)].before(host); } else { host.remove(); } }</script>{summary.disclosure && <p class="disclosure"><strong>Disclosure:</strong> {summary.disclosure}</p>}<InlineArticleAd /><ShareLinks title={summary.title} url={new URL(summary.url, Astro.site).toString()} /><footer class="article-footer"><div><strong>Topics</strong>{summary.tags.map((tag) => <a href={tag.url}>#{tag.name}</a>)}</div><a href="/disclaimer/">Educational content, not financial advice →</a></footer><PreviousNext post={summary} all={summaries} /><BottomBannerAd /></article>{related.length > 0 && <section class="container related"><p class="eyebrow">Continue reading</p><h2>Related research</h2><div class="grid">{related.map((post) => <ArticleCard {post} />)}</div></section>}</BaseLayout>
31+
<BaseLayout title={post.data.seo.title ?? summary.title} description={post.data.seo.description ?? summary.description} noindex={post.data.seo.noindex} jsonLd={schema}><article class="container article" data-pagefind-body><nav class="crumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href={summary.category.url}>{summary.category.name}</a><span>/</span><span aria-current="page">{summary.title}</span></nav><header><p class="eyebrow" data-pagefind-meta="category">{summary.category.name}</p><h1>{summary.title}</h1><p class="dek">{summary.description}</p><div class="byline" data-pagefind-ignore><AuthorByline author={summary.author} /><span>·</span><time datetime={summary.publishedAt.toISOString()}>{formatDate(summary.publishedAt)}</time>{summary.updatedAt && <><span>·</span><span>Updated {formatDate(summary.updatedAt)}</span></>}<span>·</span><span>{summary.readingTime} min read</span></div></header><figure><Image src={summary.cover.image} alt={summary.cover.alt} widths={[800, 1200, 1600]} sizes="(max-width: 800px) 100vw, 800px" loading="eager" fetchpriority="high" inferSize />{summary.cover.caption && <figcaption>{summary.cover.caption}</figcaption>}</figure>{headings.length >= 3 && <TableOfContents {headings} />}<div class="prose"><Content components={{ Callout, ProsCons, ComparisonTable, FAQ }} /></div><div id="mid-article-ad"><MidArticleAd /></div><script>const host = document.getElementById('mid-article-ad');const prose = host && host.closest('.article') && host.closest('.article').querySelector('.prose');if (host && prose) { const headings = prose.querySelectorAll('h2'); if (headings.length >= 3) { headings[Math.floor(headings.length / 2)].before(host); } else { host.remove(); } }</script>{summary.disclosure && <p class="disclosure"><strong>Disclosure:</strong> {summary.disclosure}</p>}<InlineArticleAd /><ShareLinks title={summary.title} url={new URL(summary.url, Astro.site).toString()} /><footer class="article-footer"><div><strong>Topics</strong>{summary.tags.map((tag) => <a href={tag.url}>#{tag.name}</a>)}</div><a href="/disclaimer/">Educational content, not financial advice →</a></footer><PreviousNext post={summary} all={summaries} /><BottomBannerAd /></article>{related.length > 0 && <section class="container related"><p class="eyebrow">Continue reading</p><h2>Related research</h2><div class="grid">{related.map((post) => <ArticleCard {post} />)}</div></section>}</BaseLayout>
3232
<style>.article{max-width:52rem;padding-top:2.5rem}.crumbs{display:flex;gap:.5rem;overflow:hidden;color:var(--muted);font-size:.82rem;white-space:nowrap}.crumbs a{color:var(--accent)}.article header{margin:2rem 0}.article h1{margin:.3rem 0 1rem;font:800 clamp(2.35rem,6vw,4.5rem)/1.02 var(--font-display);letter-spacing:-.045em}.dek{max-width:43rem;color:var(--muted);font-size:1.2rem}.byline{display:flex;flex-wrap:wrap;gap:.45rem;color:var(--muted);font-size:.88rem}.article figure{margin:2rem 0}.article figure :global(img){width:100%;border-radius:1rem}.article figcaption{margin-top:.5rem;color:var(--muted);font-size:.8rem}.disclosure{padding:1rem;border-radius:.7rem;background:var(--surface-raised);color:var(--muted)}.article-footer{display:flex;justify-content:space-between;gap:1rem;margin:3rem 0;padding:1.25rem 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);font-size:.9rem}.article-footer div{display:flex;flex-wrap:wrap;gap:.5rem}.article-footer div a{color:var(--accent)}.article-footer>a{color:var(--muted)}.related{margin-top:4rem}.related h2{margin:.2rem 0 1rem;font:800 2rem var(--font-display)}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}@media(max-width:700px){.article-footer{flex-direction:column}.grid{grid-template-columns:1fr}}</style>

0 commit comments

Comments
 (0)