Skip to content

[site-audit] Weekly site health audit β€” 2026-04-02Β #1782

@github-actions

Description

@github-actions

Automated audit covering structured data, accessibility contrast, SEO meta tags, heading hierarchy, image alt text, dead code, and test health.


1. Structured Data

πŸ”΄ High β€” BlogPosting.description can serialize as undefined

File: src/utils/structured-data.ts:119

description is an optional parameter with no fallback. When absent it is written directly into the JSON-LD object, producing "description": undefined which is dropped by JSON.stringify β€” leaving the required Schema.org field missing from the output.

Fix:

description: description ?? '',

🟑 Low β€” BreadcrumbList last item is missing the item URL property

File: src/utils/structured-data.ts:156-165

The current page breadcrumb only has position and name. Schema.org recommends each ListItem include an item (URL) property; omitting it on the last element is technically valid but can reduce rich-result eligibility.

Fix: Pass the canonical URL as item for the current-page crumb, or add an id field.


2. Accessibility β€” Contrast

All findings below fail WCAG AA (4.5:1 for normal text). The site's custom palette: --color-gray-400: #ced4da (β‰ˆ3.5:1 on white), --color-gray-500: #adb5bd (β‰ˆ3.0:1 on white). dark:text-gray-400 on typical dark backgrounds also fails.

πŸ”΄ Critical β€” text-gray-500 on readable body text

File Line Element / Context
src/components/Breadcrumbs.astro 20 (nav) breadcrumb text (text-gray-500 dark:text-gray-400)
src/components/SearchModal.astro 363 "No results found" empty-state message
src/components/KeepReading.astro 61 "More to explore" section label

Fix: Replace text-gray-500 with text-gray-600 (β‰ˆ7.3:1) for these text nodes.

πŸ”΄ Critical β€” dark:text-gray-400 on readable text (dark mode)

dark:text-gray-400 (#ced4da on a dark surface) fails WCAG AA across multiple files:

File Lines Context
src/components/Breadcrumbs.astro 20, 26 Navigation / current-page crumb
src/components/DiscussLinks.astro 21, 27, 36, 45 "Discuss on" label + social links
src/components/SubscribeCta.astro 10 RSS subscribe call-to-action text
src/components/SearchModal.astro 363, 394 Empty state + search-result excerpts
src/pages/about.astro 50, 53, 57, 61, 83, 87 At-a-glance labels and post dates
src/pages/index.astro 140 Feed subscription description

Fix: Replace dark:text-gray-400 with dark:text-gray-300 on all readable-text nodes.

🟒 Low β€” Acceptable low-contrast uses (no action required)

  • SearchModal.astro search icon, placeholder:text-gray-400 (placeholder), keyboard shortcut badge β€” all exempted by WCAG 1.4.3.
  • PostLayout.astro:190 arrow aria-hidden="true" β€” decorative, exempt.
  • contact.astro emoji bullets β€” decorative, exempt.

3. SEO Meta Tags

βœ… No critical issues

All Open Graph tags (og:image:width, og:image:height, og:image:alt), Twitter Card tags (twitter:image:alt, twitter:creator), canonical URL, and meta description are present. OG image and canonical are absolute URLs. The @benbalter twitter handle format is correct per Twitter's spec.

🟒 Low β€” Missing og:article:section / og:article:tag on article pages

File: src/layouts/BaseLayout.astro:127-133

When type === 'article', og:article:section and og:article:tag are absent. These are optional but improve discoverability on social platforms.

🟒 Low β€” No explicit robots meta tag for indexable pages

File: src/layouts/BaseLayout.astro

Pages with noindex=false rely on the browser default. Adding an explicit content="index, follow" prevents ambiguity with some crawlers.


4. Heading Hierarchy

πŸ”΄ High β€” Headings skip levels (h1 β†’ h3) on three pages

src/pages/fine-print.astro (lines 21, 33, 45, 57, 69): All section headings are <h3> with no <h2> present. The page layout supplies the <h1>, so sections jump directly from h1 to h3.

Fix: Wrap the disclaimer cards in an <h2> section, then keep each card heading as <h3>:

<h2>Disclaimers</h2>
<h3>Personal opinions</h3>
…
<h2>Licensing</h2>

src/pages/talks.astro (lines 122, 133): Category headings are <h3> (should be <h2>); individual talk titles are <h4> (should be <h3>).

src/pages/other-recommended-reading.astro (lines 75, 100): Same pattern β€” book category headings are <h3> and should be <h2>; individual book headings are <h4> and should be <h3>.

Fix for both: Promote category headings from h3 β†’ h2 and item headings from h4 β†’ h3.

🟑 Medium β€” Resume page has deep nesting (h2 β†’ h3 β†’ h4)

src/pages/resume.astro (lines 69, 73, 78): Experience <h2> β†’ Employer <h3> β†’ Position title <h4>. The depth isn't invalid but position titles (<h4>) are fragile for screen-reader navigation. Consider flattening to h2 β†’ h3 only.


5. Image Alt Text

βœ… No issues found

All (img), (Image), and (Picture) tags have descriptive alt attributes. No findings.


6. Dead Code

🟑 Medium β€” Two components have no consumers

src/components/CodeBlock.astro: Zero imports found anywhere in the codebase. The site uses astro-expressive-code (configured in astro.config.mjs:138) for all syntax highlighting; this file appears to be superseded legacy code.

src/components/ReadingListCard.astro: Zero imports found. ReadingList.astro renders reading-list items inline rather than delegating to this subcomponent.

Fix: Delete both files, or add a comment marking them intentionally reserved if that is the case.


7. Test Health

πŸ”΄ High β€” archived-posts.test.ts tests a non-existent export

File: src/utils/archived-posts.test.ts:30-106

The test file exercises a filtering function that does not exist as an exported utility. There is no src/utils/archived-posts.ts source file. The tests construct the logic inline, which means they verify nothing about the production code paths in index.astro.

Fix: Either extract the post-filtering logic into src/utils/archived-posts.ts and export it, or remove the test file.

🟑 Medium β€” Vague assertion in reading-time.test.ts

File: src/utils/reading-time.test.ts:88

expect(result).toBeGreaterThan(0);

Because calculateReadingTime returns Math.max(1, …) the minimum is always 1, so this assertion can never fail regardless of the implementation. The HTML input on lines 74-87 has a deterministic word count; the test should assert the exact expected value.

Fix:

expect(result).toBe(1); // ~19 words at 200 WPM rounds to 1 minute

🟒 Low β€” Misleading comment in reading-time.test.ts:50

The comment implies 6 words but the implementation strips backticks from `console.log()`, producing 7 words. The assertion (toBe(1)) still passes because the minimum reading time is 1 minute, masking the discrepancy.

🟒 Low β€” structured-data.test.ts uses config-dependent assertions

Several assertions verify that the schema contains the current config values (e.g. GitHub username) rather than verifying the structure is well-formed. If a config key is renamed these tests would still pass while the schema silently broke.


Summary

Priority Category Count
πŸ”΄ Critical/High Contrast (readable text) 3 groups (~15 instances)
πŸ”΄ High Heading hierarchy 3 pages
πŸ”΄ High Test health 1 missing source file
🟑 Medium Heading hierarchy 1 page
🟑 Medium Test health 1 vague assertion
🟑 Medium Dead code 2 components
πŸ”΄ High Structured data 1 (description undefined)
🟒 Low Structured data, SEO, tests 5 minor items

Generated by Weekly Site Audit Β· β—·

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions