Skip to content

Codebase due-diligence cleanup: issues #31–#38 - #47

Merged
plx merged 9 commits into
mainfrom
plx/codebase-due-diligence
Jun 21, 2026
Merged

Codebase due-diligence cleanup: issues #31–#38#47
plx merged 9 commits into
mainfrom
plx/codebase-due-diligence

Conversation

@plx

@plx plx commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Works through GitHub issues #31#38 from the codebase due-diligence pass — one focused, independently-verified commit per issue.

Commits (one per issue)

Issue Type Change
#31 perf Drop unused item.render() passes on the home / briefs / category list pages (the <Content /> was never used)
#32 perf Memoize getCategory() so each category.yaml is read/parsed at most once per build instead of once per brief card
#33 fix Correct readingTime to max(1, ceil(words / 200)) (no more blanket +1 minute); add a Vitest harness + edge-case tests
#34 refactor Narrow getProjectCardProps to CollectionEntry<"projects"> and extract a shared getStandardCardProps
#35 chore Remove the unused, broken CallToAction.astro (imported a non-existent SCSS path)
#36 fix Fix dateRange formatting and open-ended end dates (MMM YYYY - Present); add branch tests
#37 feat Validate demoURL / repoURL / ogImage as z.string().url() in the content schema
#38 refactor Centralize the draft-filter + date-desc-sort (+ optional slice) pipeline into src/lib/collections.ts and route every page / getStaticPaths / RSS through it

Test infrastructure

The repo previously had no unit-test runner (only Playwright QA). #33/#36 add a minimal Vitest setup:

  • vitest devDependency + test:unit script
  • vitest.config.ts scopes Vitest to src/**/*.test.ts so it never collides with the Playwright tests/*.spec.ts suites
  • src/lib/utils.test.ts (15 assertions covering reading-time and date-range edge cases)
  • a Run Unit Tests step added to the shared build.yml CI workflow (runs on PR validation and pre-deploy) and to the local test:ci aggregates

Validation

Closes #31
Closes #32
Closes #33
Closes #34
Closes #35
Closes #36
Closes #37
Closes #38

🤖 Generated with Claude Code

plx and others added 9 commits June 20, 2026 04:44
The home page, briefs index, and per-category brief pages each called
`item.render()` for every entry and threaded the resulting `Content`
component through, even though those list views only render card data
derived from frontmatter (title/description/link). The `<Content />`
component was never used, so every render did wasted markdown/MDX
processing at build time.

Drop the `Promise.all(... item.render())` passes and feed the collection
entries directly into the card helpers. Sort/filter/slice behavior is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`getCategory` performed synchronous `existsSync` + `readFileSync` (and a
YAML parse) on every call. Brief card generation calls it once per card,
so a category's `category.yaml` was re-read and re-parsed many times per
build.

Memoize resolved category metadata in a module-level `Map`, keyed by
slug + path, so each category's YAML is read and parsed at most once per
build. Missing/invalid-YAML fallback to default metadata is preserved
(the resolved default is what gets cached).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`readingTime` added an unconditional `+1` minute before rounding, so
every post's estimate was inflated (e.g. a 200-word post read as
"2 min read"). Switch to the standard formula `max(1, ceil(words / 200))`
and normalize empty/whitespace-only content to "1 min read".

This is also the first code in the repo to get unit coverage, so add a
minimal Vitest harness:

- add `vitest` (devDependency) and a `test:unit` script
- `vitest.config.ts` scopes Vitest to `src/**/*.test.ts` so it doesn't
  collide with the Playwright `tests/*.spec.ts` suites
- `src/lib/utils.test.ts` covers the reading-time edge cases called out
  in the issue (0/1/199/200/201/399/400 words, empty content, HTML
  stripping, round-up)
- wire `npm run test:unit` into the shared CI workflow and the local
  `test:ci` aggregates

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`getProjectCardProps` accepted `CollectionEntry<"blog"> | CollectionEntry<"projects">`
despite being the project helper, and duplicated `getBlogCardProps`
verbatim. Narrow its input to `CollectionEntry<"projects">` and route both
the blog and project helpers through a shared internal `getStandardCardProps`
mapping. `getBriefCardProps` stays specialized because it adds the
category-prefix behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`CallToAction.astro` was unused dead code left over from the
accessible-astro-starter template. It imported `../assets/scss/base/mixins`,
a path that does not exist in this project (there is no `src/assets`
directory), so it would have failed to build the moment anything imported
it. Its default copy/links also pointed at the upstream starter theme.
Remove it rather than retrofit a component nothing references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`dateRange` concatenated month and year with no separator ("Jan2020") and,
when `endDate` was omitted, produced "undefinedundefined" because the end
month/year were never assigned.

Rework it to format each endpoint as `MMM YYYY` (deterministic en-US
locale, matching `formatDate`) and join with " - ". The end label is the
provided string verbatim when a string is passed, a formatted `MMM YYYY`
when a Date is passed, and defaults to "Present" when omitted. Add tests
covering all three branches and asserting no `undefined` fragments appear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
URL-valued frontmatter fields were typed as plain `z.string()`, so a
malformed link in frontmatter sailed through validation and could emit a
broken link into a generated page.

Tighten the project `demoURL`/`repoURL` fields and the `ogImage` fields
(blog/briefs/projects) to `z.string().url()`. Both are semantically
absolute URLs — `demoURL`/`repoURL` are rendered as external links and OG
images must be absolute per the Open Graph protocol — so the absolute-only
policy `.url()` enforces is the intended one; relative paths are rejected.
`.optional()` is preserved. All existing project content already uses
absolute https URLs, so `astro check` and link validation still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "fetch a collection, drop drafts, sort newest-first (optionally
sliced)" pipeline was copy-pasted across every list page, every detail
route's `getStaticPaths`, and the RSS endpoint. Any change to the
draft/sort/slice policy had to be made in ~9 places.

Add `src/lib/collections.ts` exposing:
- `published(entries)` — drop drafts
- `byDateDesc(entries)` — date-descending sort (non-mutating)
- `getPublishedCollection(name, limit?)` — the common fetch+filter+sort,
  with an optional limit for homepage previews

Route every consumer through these helpers. The home/blog/briefs/projects
index pages and the blog/projects/briefs detail routes use
`getPublishedCollection`; the per-category brief page and the RSS endpoint
(which filter/merge before sorting) compose `published` + `byDateDesc`.
Ordering, draft exclusion, and slice counts are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the issue #31-#38 work up to date with main, which had advanced
with the April-2026 dependency refresh and tooling upgrades (eslint 10,
typescript 6, cspell 10, @types/node 24, .nvmrc pin + CI node-version-file).

Conflicts were limited to the test-harness additions from #33:
- package.json: kept main's dependency set (including the dropped
  eslint-plugin-jsx-a11y and the @astrojs/check typescript override) and
  re-added the `vitest` devDependency + `test:unit`/`test:ci` script wiring.
- package-lock.json: regenerated from main's lockfile with `npm install`.
- .github/workflows/build.yml: auto-merged (main's node-version-file +
  the new "Run Unit Tests" step).

Re-validated the full branch under the upgraded toolchain: lint, 15 vitest
tests, tsc 6 typecheck (app + playwright), astro build (0 errors), cspell 10
source + HTML, and internal link validation all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Spell check (source)
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude

claude Bot commented Jun 20, 2026

Copy link
Copy Markdown

Code Review — Codebase Due-Diligence Cleanup (#31#38)

Overall this is a well-structured, focused PR. The atomic-commit-per-issue approach makes each change easy to reason about independently, and the test harness addition (Vitest for unit tests, scoped away from Playwright) is a solid foundation. Findings below, roughly ordered by severity.


Issues

category.ts shows as a binary diff with 0 additions / 0 deletions

The file is flagged as modified (changeType: MODIFIED) but the GitHub API reports additions: 0, deletions: 0 and git diff renders it as a binary comparison. This means the memoization change from issue #32 is not visible in the diff at all. It is possible this is just a line-ending normalization artifact from the merge commit, but it is worth verifying:

  • Did the memoization land in a prior commit that the merge squashed away?
  • Or does category.ts on the branch actually differ from what is on main?

If the memoization is genuinely absent, issue #32 is not addressed by this PR. If it is present but git cannot diff it, the binary encoding problem should be fixed before merge (e.g., normalize line endings, remove BOM).


z.string().url() may silently break existing content

z.string().url() (Zod's URL validator) requires an absolute URL with a valid scheme. Any existing frontmatter that uses a relative path (/assets/og.png), a protocol-relative URL (//example.com/img.png), or a bare domain (github.com/plx) will now fail schema validation at build time and potentially cause astro build to error. Worth auditing existing content before this lands — especially ogImage, which is the most likely field to hold a path rather than a full URL.


Missing newline at end of src/lib/utils.ts

The diff ends with No newline at end of file after the dateRange function. Minor, but conventionally all text files should end with a newline.


Observations (non-blocking)

readingTime fix — Correct. The old formula (wordCount / 200) + 1 always added a phantom minute. The new Math.max(1, Math.ceil(wordCount / 200)) is the standard formula. The tag-stripping change from replace(/<[^>]+>/g, "") to replace(/<[^>]+>/g, " ") also correctly prevents adjacent words from being joined when tags are removed (e.g. <em>hello</em><em>world</em> would previously produce "helloworld").

dateRange fix — The original code produced ${startMonth}${startYear} (no space between month and year) and relied on endMonth/endYear variables that were undefined when the end date was omitted. The rewrite is clean and the formatMonthYear helper removes the duplication.

collections.ts — The published / byDateDesc / getPublishedCollection split is a good decomposition: the first two are pure and easily testable, the third wraps the async getCollection call. The one subtle choice is that getPublishedCollection always fetches all entries before slicing — fine for a static-site build where all content is local, just worth noting.

Test coverage — The 15 assertions cover the two fixed functions well. published and byDateDesc from collections.ts are not tested, but they are one-liners that are trivially correct, so that trade-off is fine.

getProjectCardProps narrowing — The original signature CollectionEntry<"blog"> | CollectionEntry<"projects"> was clearly wrong for a function named getProjectCardProps. The narrowing to CollectionEntry<"projects"> is correct.

rss.xml.ts — Still imports getCollection directly, then applies published + byDateDesc from the new module. This is correct: the RSS feed merges two collections before sorting, which getPublishedCollection does not support (single-collection only). The approach is the right one given the current API surface.


Summary

The binary diff on category.ts is the main thing to resolve before merging — it either hides the #32 memoization change or indicates an encoding issue that will confuse future git blame/git diff runs. The z.string().url() strictness is worth a quick content audit to confirm no existing frontmatter uses relative paths for those fields. Everything else looks good.

@plx
plx merged commit e0aff25 into main Jun 21, 2026
3 checks passed
@plx
plx deleted the plx/codebase-due-diligence branch June 21, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment