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 Β· β·
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.descriptioncan serialize asundefinedFile:
src/utils/structured-data.ts:119descriptionis an optional parameter with no fallback. When absent it is written directly into the JSON-LD object, producing"description": undefinedwhich is dropped byJSON.stringifyβ leaving the required Schema.org field missing from the output.Fix:
π‘ Low β BreadcrumbList last item is missing the
itemURL propertyFile:
src/utils/structured-data.ts:156-165The current page breadcrumb only has
positionandname. Schema.org recommends eachListIteminclude anitem(URL) property; omitting it on the last element is technically valid but can reduce rich-result eligibility.Fix: Pass the canonical URL as
itemfor the current-page crumb, or add anidfield.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-400on typical dark backgrounds also fails.π΄ Critical β
text-gray-500on readable body textsrc/components/Breadcrumbs.astro(nav)breadcrumb text (text-gray-500 dark:text-gray-400)src/components/SearchModal.astrosrc/components/KeepReading.astroFix: Replace
text-gray-500withtext-gray-600(β7.3:1) for these text nodes.π΄ Critical β
dark:text-gray-400on readable text (dark mode)dark:text-gray-400(#ced4da on a dark surface) fails WCAG AA across multiple files:src/components/Breadcrumbs.astrosrc/components/DiscussLinks.astrosrc/components/SubscribeCta.astrosrc/components/SearchModal.astrosrc/pages/about.astrosrc/pages/index.astroFix: Replace
dark:text-gray-400withdark:text-gray-300on all readable-text nodes.π’ Low β Acceptable low-contrast uses (no action required)
SearchModal.astrosearch icon,placeholder:text-gray-400(placeholder), keyboard shortcut badge β all exempted by WCAG 1.4.3.PostLayout.astro:190arrowaria-hidden="true"β decorative, exempt.contact.astroemoji 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@benbaltertwitter handle format is correct per Twitter's spec.π’ Low β Missing
og:article:section/og:article:tagon article pagesFile:
src/layouts/BaseLayout.astro:127-133When
type === 'article',og:article:sectionandog:article:tagare absent. These are optional but improve discoverability on social platforms.π’ Low β No explicit
robotsmeta tag for indexable pagesFile:
src/layouts/BaseLayout.astroPages with
noindex=falserely on the browser default. Adding an explicitcontent="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>: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βh2and item headings fromh4β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 descriptivealtattributes. No findings.6. Dead Code
π‘ Medium β Two components have no consumers
src/components/CodeBlock.astro: Zero imports found anywhere in the codebase. The site usesastro-expressive-code(configured inastro.config.mjs:138) for all syntax highlighting; this file appears to be superseded legacy code.src/components/ReadingListCard.astro: Zero imports found.ReadingList.astrorenders 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.tstests a non-existent exportFile:
src/utils/archived-posts.test.ts:30-106The test file exercises a filtering function that does not exist as an exported utility. There is no
src/utils/archived-posts.tssource file. The tests construct the logic inline, which means they verify nothing about the production code paths inindex.astro.Fix: Either extract the post-filtering logic into
src/utils/archived-posts.tsand export it, or remove the test file.π‘ Medium β Vague assertion in
reading-time.test.tsFile:
src/utils/reading-time.test.ts:88Because
calculateReadingTimereturnsMath.max(1, β¦)the minimum is always1, 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:
π’ Low β Misleading comment in
reading-time.test.ts:50The 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.tsuses config-dependent assertionsSeveral 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
descriptionundefined)