Conversation
- Introduced multiple "Today I Learned" articles covering diverse subjects including AstroWind, Vercel Speed Insights, Pagefind search integration, Notion-powered contact forms, and more. - Each entry provides insights, implementation details, and key takeaways to enhance web development practices. ## New Entries - **AstroWind**: Starting with a well-structured template for custom website development. - **Vercel Speed Insights**: Integrating real user metrics for performance monitoring. - **Pagefind**: Implementing client-side search for static sites. - **Notion Contact Form**: Building a serverless form submission system. - **React Email**: Creating and sending HTML emails with React components. - **Vercel Bot Protection**: Enhancing form security without CAPTCHAs. - **Cloudinary**: Dynamic image optimization for web performance. - **Accessibility Testing in CI/CD**: Automating accessibility checks in GitHub Actions. - **Cache-Control Strategy**: Fine-tuning caching for performance and freshness. - **TIL Content Collection**: Building a structured content collection in Astro. - **Notion Content Loader**: Custom loader for fetching Notion database pages. - **Internal Linking for SEO**: Implementing automated internal linking strategies. - **Starwind Pagination and Breadcrumbs**: Adding accessible navigation components. - **URL Sanitization**: Fixing vulnerabilities in URL validation. - **SSR Flash on Accordions**: Preventing state flash during hydration. - **Astro Actions with React**: Modernizing form handling with type-safe actions. - **Editorial Typography System**: Implementing a cohesive typography strategy. - **Brand Color Refresh**: Systematically updating the color palette for accessibility.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds 36 new TIL markdown articles under src/content/til covering Astro integrations, performance (RUM, cache, search), accessibility/testing pipelines, UI/typography patterns, image tooling (Cloudinary), backend patterns (webhooks, schedulers), and developer workflows; all changes are documentation-only examples and guides with no source-code exports modified. Changes
Sequence Diagram(s)(Skipped — changes are documentation-only and do not introduce new multi-component control flows requiring sequence visualization.) Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In @src/content/til/2025-05-27-notion-contact-form.md:
- Around line 27-47: The POST handler example is missing an import for redirect,
lacks input validation, and has no error handling; update the top imports to
include redirect, validate required form fields from request.formData() (e.g.,
ensure name, email, message are present and non-empty before calling
notion.pages.create), wrap the notion.pages.create call in a try-catch to handle
Notion API errors, and on success return redirect('/thank-you') while on
validation failure or API error return an appropriate response (e.g., 400 for
missing fields, 500 for API errors) with useful error messages.
In @src/content/til/2025-06-25-bot-protection.md:
- Around line 49-57: The POST handler currently reads the wrong header and
treats it as a simple "verified" string; change it to read the BotID token from
the x-is-human header and validate it using Vercel's BotID verification
utilities (e.g., call checkBotId or botid verifier inside POST, or wrap the
route with withBotId middleware) instead of string comparison; if verification
fails, return a 403 Response, otherwise continue to process the legitimate
submission.
In @src/content/til/2025-12-23-astro-actions-react-forms.md:
- Around line 38-62: The ContactForm example uses the wrong state shape and
error handling; update it to call
useActionState(withState(actions.submitContact), { data: null, error: undefined
}) (wrap the action with withState), import and use isInputError to check
state.error before reading validation details, and replace state.errors?.name
with state.error.fields.name (and similarly for email and message) after the
isInputError guard; also add the missing email and message inputs and display
their field errors using state.error.fields.<fieldName>.
In @src/content/til/advanced-focus-management.md:
- Around line 17-57: Add a getFocusableElements(container) method to the
FocusManager class (used by trapFocus and handleTabKey) that accepts a container
element, queries for common focusable selectors (e.g., button, [href], input,
select, textarea, [tabindex]:not([tabindex="-1"])), returns an Array of
elements, and gracefully handles a null/undefined container by returning an
empty array so trapFocus, handleTabKey, and releaseFocus work reliably with the
modalStack.
- Around line 124-135: The code calls announce(node.dataset.announce) inside the
MutationObserver callback but announce is undefined; either add a minimal
accessible announcement utility named announce that accepts a string and posts
it to an aria-live region (create or select an element with
aria-live="polite"/"assertive", set its textContent, append if missing) or
document that announce is an external dependency and import it; update the file
to include that announce function (used by observer/MutationObserver and reading
node.dataset.announce) or add an explicit import statement and a short comment
pointing to the utility.
In @src/content/til/astro-content-collections-zod.md:
- Around line 17-37: The example uses Astro 4 schema syntax (schema: ({ image })
=> ...) and the image() helper; update the tilCollection definition to Astro 5
style by removing the function wrapper and supplying a direct Zod schema to
schema (use z.coerce.date() for date), replace image().optional() with an
appropriate field type such as z.string().optional(), and add a loader (e.g.,
use glob in loader) to match Astro 5's defineCollection API while keeping the
collection name til and the defineCollection/collections symbols.
In @src/content/til/cloudinary-image-presets.md:
- Around line 21-27: Add the missing type definitions used by the presets
example: declare an interface CloudinaryTransformOptions with optional fields
width and height (number|string), crop, quality, gravity, and format (strings),
and declare the Preset union type
('portfolio'|'thumbnail'|'hero'|'portrait'|'responsive'); then keep the existing
const presets: Record<Preset, CloudinaryTransformOptions> as-is so the example
is complete and type-safe.
- Around line 35-53: The function getCloudinaryImageUrl references cloudName but
it is not declared or imported; fix by sourcing cloudName (either accept it as
an added parameter to getCloudinaryImageUrl, or load it from configuration/env
and export/import it where this file expects it). Update the function signature
or module to obtain cloudName, ensure any callers are updated if you change the
signature, and keep existing behavior using presets and Preset; reference
getCloudinaryImageUrl, cloudName, presets and Preset when making the change.
- Around line 61-70: The getResponsiveImageUrls function calls getCloudinaryUrl
but the actual helper is named getCloudinaryImageUrl and the ResponsiveUrls
return type is missing; fix by calling getCloudinaryImageUrl(publicId, { width:
w }) inside getResponsiveImageUrls (or rename the helper to getCloudinaryUrl if
you prefer consistency), and add a ResponsiveUrls type/interface (e.g.,
interface ResponsiveUrls { srcSet: string; sizes: string } or export type
ResponsiveUrls = { srcSet: string; sizes: string }) so the function signature
has a valid return type; update any imports/exports accordingly to keep names
consistent.
In @src/content/til/content-readability-optimization.md:
- Around line 139-146: The filter in suggestSimplifications incorrectly uses
text.toLowerCase().includes(complex) which matches substrings and causes false
positives; change it to test whole-word matches by creating a case-insensitive
regex with word boundaries for each key in simplifications (e.g., new
RegExp('\\b' + escapeRegExp(complex) + '\\b', 'i')), add an escapeRegExp helper
to safely escape special regex chars, and use regex.test(text) instead of
includes while keeping the rest of the mapping the same.
- Around line 105-122: The auditContent function calls analyzeReadability(body)
but analyzeReadability is not imported; add an import for the analyzeReadability
function at the top of this script (the same module that defines
analyzeReadability in the codebase) so auditContent can call it—ensure the
import uses the correct exported name (analyzeReadability) and path to the
module that implements it.
- Around line 30-48: analyzeReadability calls an undefined helper
countSyllables, causing runtime failure; add a countSyllables(word) function
above analyzeReadability that implements a simple heuristic (lowercase the word,
strip non-letters, count vowel groups as syllables, subtract for trailing silent
'e' and common diphthongs, and ensure a minimum of 1 syllable) so syllable
totals and flesch calculation work correctly; ensure the function name is
exactly countSyllables and export it if the module pattern requires it so
analyzeReadability can call it.
In @src/content/til/notion-cms-astro-loader.md:
- Around line 1-7: Rename the file to match the TIL naming convention by adding
the date prefix: change the filename from notion-cms-astro-loader.md to
2025-01-03-notion-cms-astro-loader.md so it aligns with entries like
2025-08-15-internal-linking-seo.md and 2025-08-13-notion-astro-loader.md; ensure
any internal links or references pointing to notion-cms-astro-loader.md are
updated to the new filename to avoid broken links.
In @src/content/til/opentelemetry-wide-events.md:
- Around line 127-144: The POST handler currently assumes req.json() succeeds,
that process.env.AXIOM_TOKEN exists, and that the fetch to Axiom will always
succeed; wrap the handler body in a try/catch around await req.json() and the
fetch, validate the presence of process.env.AXIOM_TOKEN at the top of the POST
function (return a 500/401 response if missing), check the fetch response and
throw or return a non-200 Response when the remote ingest fails, and log errors
(including the caught exception and upstream response text) before returning an
appropriate error Response from POST.
In @src/content/til/stripe-webhook-processing.md:
- Around line 99-126: The example mixes SolidJS and React patterns; either mark
it explicitly as SolidJS or convert it to React. If keeping SolidJS, add a
top-line comment like "// SolidJS example" and ensure imports/use of
createQuery, Show, and useSearchParams are from SolidJS/TanStack Solid adapters;
keep CheckoutSuccess, createQuery, Show, checkSubscriptionStatus, delay,
SyncingIndicator, and SuccessMessage as-is. If converting to React, replace
createQuery with useQuery from TanStack React, replace Solid's useSearchParams
and Show usage with React Router's useSearchParams and conditional rendering
(ternary or &&) around SuccessMessage, and keep polling logic using
checkSubscriptionStatus and delay but inside the useQuery queryFn; update
imports accordingly.
- Around line 35-60: The current handleWebhook action performs the idempotency
check via internal.stripe.getProcessedEvent and the write via
internal.stripe.markEventProcessed outside a single transaction, causing a
TOCTOU race; fix this by creating a Convex mutation (e.g., processWebhookEvent)
that takes the event, performs the existing check and the mark-as-processed
insert/update atomically using ctx.db (or a unique index/upsert on eventId), and
calls processEvent from inside that mutation; then change handleWebhook to
simply invoke that mutation with the event so the read-modify-write happens
inside one transactional mutation.
In @src/content/til/timezone-aware-cron-jobs.md:
- Around line 29-37: The current code incorrectly uses object-literal syntax for
Convex cron setup; replace that with a crons instance by calling cronJobs() and
then register the job via instance methods (e.g.,
crons.cron("dailyEmailDispatch", "0 * * * *", ...) or
crons.hourly("dailyEmailDispatch", ...)); also pass a Convex FunctionReference
for the handler (e.g., internal.dailyEmailHandler) instead of a raw function
object (dailyEmailHandler) so the job is registered correctly.
🟡 Minor comments (3)
src/content/til/2025-06-29-cache-control-strategy.md-22-46 (1)
22-46: Remove the JSON comment or use a supported syntax.The code block contains a JSON comment (
// vercel.json), which is not valid JSON and may mislead readers attempting to copy-paste the configuration directly. Consider either removing the comment or using a Markdown code block with a language-agnostic syntax.💬 Proposed fix
Option 1: Remove the comment
```json -// vercel.json {Option 2: Use a separate callout before the code block
+For `vercel.json`: ```json {src/content/til/2025-12-31-brand-color-refresh.md-44-49 (1)
44-49: Undefined function reference:getContrastRatio()needs implementation or attribution.The TypeScript example calls
getContrastRatio()on line 46 without defining it or indicating which library provides it. Readers won't know how to implement this check.🔧 Suggested fix: Reference or provide implementation
Add context above or below the code block:
// Using a library like polished or wcag-contrast: import { getContrastRatio } from 'polished'; // or similar function meetsWCAG(textColor: string, bgColor: string): boolean { const ratio = getContrastRatio(textColor, bgColor); return ratio >= 4.5; // WCAG 2.1 AA standard for normal text } // Or implement using relative luminance (WCAG formula) function getContrastRatio(color1: string, color2: string): number { // Implementation using relative luminance calculation // See: https://www.w3.org/WAI/WCAG21/Techniques/general/G17.html }Also consider mentioning that 7:1 is required for AAA compliance (enhanced contrast).
src/content/til/2025-06-25-cloudinary-integration.md-32-39 (1)
32-39: Add missingfillimport to the code example.The code uses
fill()without importing it. Add this import at the top of the file:import { fill } from "@cloudinary/url-gen/actions/resize";The method chain
.resize(fill().width(width))correctly uses the Cloudinary URL Gen API, but thefillfunction must be explicitly imported from theactions/resizesubmodule.
🧹 Nitpick comments (24)
src/content/til/view-transitions-api.md (2)
20-47: Clarify router library context for the example.The
useNavigate()hook (line 25) is router-library specific and varies across React Router, TanStack Router, and other navigation libraries. Readers unfamiliar with the pattern may struggle to adapt this example to their router. Consider either:
- Explicitly naming the router library you're using
- Showing a more generic example using
window.location.hrefor a generic callback as the primary example, with router-specific variants notedThis would improve the practical utility of the TIL entry across different project setups.
♻️ Router-agnostic example (alternative)
// More framework-agnostic approach export function ViewTransitionLink(props: { href: string; children: any }) { const handleClick = (e: MouseEvent) => { e.preventDefault(); if (!document.startViewTransition) { window.location.href = props.href; return; } document.startViewTransition(() => { window.location.href = props.href; }); }; return ( <a href={props.href} onClick={handleClick}> {props.children} </a> ); }Then add a note: "For client-side routers like React Router, replace
window.location.hrefwith your router's navigation function (e.g.,useNavigate()in React Router)."
112-126: Add explicit browser support information.The Browser Support section mentions feature detection but doesn't clearly state which browsers and versions actually support the View Transitions API. Readers may assume broader support than currently exists. Add a statement like:
"View Transitions API is supported in Chrome 111+, Edge 111+, and Opera 97+. Firefox and Safari do not yet support it (as of early 2026)."
This sets expectations and helps developers decide if the fallback path is sufficient for their target audience.
src/content/til/solid-primitives-storage.md (1)
61-72: Minor: Clarify SSR hydration explanation (optional enhancement).Line 69's comment "No hydration mismatch!" is correct but may be cryptic for readers unfamiliar with SSR context. Consider briefly explaining why makePersisted avoids hydration mismatches—e.g., it defers initialization until client-side to prevent state divergence between server and browser renders.
This is a minor enhancement to content clarity; the code is accurate as-is.
src/content/til/opentelemetry-wide-events.md (1)
30-65: Consider adding a note on batching/emission strategy for scale.The
RequestEventclass emits immediately onsend()(line 62). For high-traffic systems, you might want to briefly mention that production implementations often batch events before transmission to optimize throughput and reduce overhead. This would help readers contextualize when to use immediate vs. deferred emission.src/content/til/2025-06-25-bot-protection.md (1)
33-44: Make the endpoint example more generic.The firewall rule references a specific endpoint
api/submit-to-notion, which may confuse readers into thinking this endpoint is required or special. Consider using a generic placeholder or clarifying that readers should customize this path for their own endpoints.💭 Suggested clarification
Either add explanatory text before the code block:
Add to your `vercel.json`, replacing `/api/submit-to-notion` with your actual form submission endpoint:Or use a more generic placeholder:
- "source": "api/submit-to-notion" + "source": "api/contact"Then add a note that readers should customize it for their own endpoints.
src/content/til/2024-12-30-astrowind-foundation.md (1)
18-18: Remove redundant phrase "SEO optimization"."SEO" already stands for Search Engine Optimization, making the full phrase tautological. Change to simply:
**SEO** built into the layout components.src/content/til/react-email-templates.md (1)
1-10: Consider adding a "See also" section linking to simpler React Email tutorials.The PR includes
2025-05-27-react-email-welcome.md, which covers basic welcome email patterns. Since both articles target the React Email ecosystem, adding a brief "Further Reading" or "See Also" section could improve discoverability. This is optional if your site's tag-based navigation already handles this.src/content/til/2025-12-30-editorial-typography.md (1)
21-43: Well-designed typography system with accessibility-conscious defaults.The three-font strategy is pragmatic, and the CSS implementation is clean. The 1.75 line-height for body text aligns with WCAG readability guidelines. The font weight limiting (400, 500, 700) is a smart performance trade-off.
One suggestion: Consider documenting how these fonts are loaded (e.g.,
@font-facewithfont-display: swap) to prevent layout shift (CLS) during font loading.src/content/til/2025-08-15-internal-linking-seo.md (1)
43-58: Readability analysis approach is reasonable, but helper functions lack context.The code references
countWords(),calculateFleschScore(), andgetImprovementSuggestions()without implementation. For a TIL documentation file, this is acceptable since the focus is on the pattern and concept, not a complete implementation. However, consider adding a brief comment indicating these are helper functions you'd need to implement or import.Optional enhancement: Add clarifying comment for helper functions
export function analyzeReadability(text: string) { const sentences = text.split(/[.!?]+/); + // Note: countWords, calculateFleschScore, and getImprovementSuggestions + // are helper functions you'll need to implement based on your readability scoring logic const avgWordsPerSentence = countWords(text) / sentences.length; return { score: calculateFleschScore(text), avgSentenceLength: avgWordsPerSentence, suggestions: getImprovementSuggestions(text), }; }src/content/til/timezone-aware-cron-jobs.md (2)
82-89: Consider completing TIMEZONE_OFFSET_MAP example.The
TIMEZONE_OFFSET_MAPis marked incomplete with// ... etc, which is reasonable for a documentation example. However, consider adding a brief comment clarifying whether readers should use a pre-built library (e.g.,Intl.DateTimeFormat) or note that a complete mapping is provided elsewhere, to guide implementation.
156-177: Batching implementation could benefit from clarification.The
processEmailCohortfunction usesPromise.all()within batches, which is correct, but the delay logic applies only between batches. Consider adding a comment clarifying whether individual email sends should also have rate-limit handling, depending on the email service provider's limits.src/content/til/temporal-api-date-handling.md (1)
70-72: Add implementation details for the MidnightInvalidationHook concept.The section presents
MidnightInvalidationHookas the key insight but provides no code example or explanation of how it works. For a technical guide, readers need to understand either the implementation or at least the mechanics—how it detects date changes and invalidates queries.Consider including a brief code example showing the hook's structure or a sentence explaining the detection mechanism (e.g., using Temporal to compare the previous cached timezone-aware date with the current one, or setting a listener for midnight transitions).
src/content/til/2025-06-27-accessibility-testing-ci.md (1)
21-38: Workflow may have race condition: preview server startup not verified before tests run.Line 35 starts the preview server with
pnpm preview &in the background, but there's no guarantee it's ready before Playwright tests begin execution on line 37. This can cause flaky test failures when the server isn't fully initialized.Consider adding a health check or delay:
🔧 Proposed improvement
- run: pnpm build - - run: pnpm preview & + - run: pnpm preview > /tmp/preview.log 2>&1 & + - run: npx wait-on http://localhost:3000 --timeout 30000 - name: Run accessibility tests run: npx playwright test accessibility.spec.tsAlternatively, use
pnpm preview --port 3000with explicit port and add retry logic in your Playwright config for improved reliability.src/content/til/wcag-accessibility-audit.md (1)
33-43: Color contrast example assumes white background but doesn't state it explicitly.The before/after CSS snippet shows color values (#999999 → #757575) with claimed contrast ratios (3.2:1 → 4.7:1), but the background color isn't specified in the code. Readers may apply these colors against different backgrounds where the ratios won't hold true.
🔧 Suggested fix: Make the background explicit
/* Before: Contrast ratio 3.2:1 against #ffffff (fails AA) */ .muted-text { color: #999999; background: #ffffff; /* or document in surrounding text */ } /* After: Contrast ratio 4.7:1 against #ffffff (passes AA) */ .muted-text { color: #757575; background: #ffffff; }Or add context: "Assuming a white background, #999999 provides only a 3.2:1 contrast ratio..."
src/content/til/accessibility-testing-pipeline-axe.md (1)
80-94: GitHub Actions workflow is valid, but may need context.The workflow assumes
npm run previewandnpm run axe:scanscripts are defined in package.json. Consider briefly documenting these script requirements for readers implementing this pattern, or link to related documentation on setup.src/content/til/accessibility-ci-cd-pipeline.md (2)
53-68: Regression detection approach is sound, butdiffoutput may be hard to parse.Testing both branches and comparing reports is the right pattern. However, line 68's
diffcommand produces text output that's difficult to parse programmatically. For actual implementation, consider usingjqto compare specific fields or a custom Node script to extract and compare accessibility scores.Example: Comparing with jq instead of diff
# Instead of: diff current/summary.json base/summary.json # Use something like: jq -n --slurpfile current current/summary.json --slurpfile base base/summary.json \ '{current: $current[0], base: $base[0], changed: ($current[0].score != $base[0].score)}'
76-85: GitHub comment posting is correct, but assumes summary.json schema.The API call and markdown formatting are properly structured. For clarity, you might document what fields are expected in summary.json (e.g., does it have a
scorefield, what are valid score ranges, etc.) so readers know what to provide from their test scripts.src/content/til/advanced-focus-management.md (3)
64-84: Clarify the context of theinitRovingTabindexfunction.This function appears to be a method or standalone utility, but it's presented without context. Clarify whether it:
- Should be a method of the
FocusManagerclass (for consistency with the earlier example), or- Is a standalone utility function that complements
FocusManager.If it's meant to be independent, consider prefixing with a clear statement like "Initialize roving tabindex on a container:" to distinguish it from the
FocusManagerpatterns.
91-110: Clarify the context of theinitGridNavigationfunction.Like
initRovingTabindex, this function's context is unclear. Is it a method ofFocusManageror a standalone utility? Consistency in presentation would help readers apply the patterns more easily. Consider grouping related functions or clearly documenting their relationship.
40-55: Document the event listener binding pattern.The
handleTabKeyarrow function field is correctly bound tothis, which works for bothaddEventListenerandremoveEventListener. However, this is a subtle JavaScript pattern that may confuse readers. Add a brief comment explaining why this works:// Arrow function field ensures 'this' is bound, allowing proper cleanup with removeEventListener handleTabKey = (e) => { // ... }src/content/til/2025-07-08-til-content-collection.md (1)
52-59: Clarify the Kanban Board data structure.The code example references a
weeksarray with entries grouped by week, but doesn't show how this data structure is created. For completeness, consider adding a brief note or code snippet showing the transformation from a flatentriesarray into aweeksarray withstartDateandentriesproperties. This would help readers understand the full implementation pattern.src/content/til/stripe-webhook-processing.md (2)
88-90: Status mapping forincompleteandpausedmay oversimplify.Both
incompleteandpausedmap topending, but they represent quite different states:
incompleteis a transitional state (payment in progress, checkout abandoned mid-flow) and typically should be treated as "not yet active."pausedis an active subscription paused by the customer or merchant, which is semantically closer to a suspension or grace state, not pending.Consider whether
pausedshould have its own status or map to something likesuspendedrather thanpending.
109-114: Polling strategy could benefit from explanation or backoff.The polling loop uses fixed 1000ms delays for 10 attempts (10 seconds total timeout). While reasonable, the constants appear arbitrary and lack rationale. Consider:
- Adding a comment explaining why 10 seconds is appropriate (webhook latency, database replication lag, etc.).
- Implementing exponential backoff (e.g., 500ms, 1s, 2s, ...) to reduce database load if polling is frequent.
This is a minor point but would improve clarity and resilience.
src/content/til/tanstack-query-server-state.md (1)
11-11: Minor grammar: consider hyphenating compound modifier.The phrase "custom data fetching utilities" could be written as "custom data-fetching utilities" to follow strict hyphenation rules for compound modifiers before a noun. This is a stylistic point and very much optional, especially in technical writing.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (36)
src/content/til/2024-12-30-astrowind-foundation.mdsrc/content/til/2025-03-23-vercel-speed-insights.mdsrc/content/til/2025-05-18-pagefind-search.mdsrc/content/til/2025-05-27-notion-contact-form.mdsrc/content/til/2025-05-27-react-email-welcome.mdsrc/content/til/2025-06-25-bot-protection.mdsrc/content/til/2025-06-25-cloudinary-integration.mdsrc/content/til/2025-06-27-accessibility-testing-ci.mdsrc/content/til/2025-06-29-cache-control-strategy.mdsrc/content/til/2025-07-08-til-content-collection.mdsrc/content/til/2025-08-13-notion-astro-loader.mdsrc/content/til/2025-08-15-internal-linking-seo.mdsrc/content/til/2025-10-05-starwind-pagination-breadcrumbs.mdsrc/content/til/2025-10-09-url-sanitization-security.mdsrc/content/til/2025-12-23-accordion-ssr-flash.mdsrc/content/til/2025-12-23-astro-actions-react-forms.mdsrc/content/til/2025-12-30-editorial-typography.mdsrc/content/til/2025-12-31-brand-color-refresh.mdsrc/content/til/accessibility-ci-cd-pipeline.mdsrc/content/til/accessibility-testing-pipeline-axe.mdsrc/content/til/advanced-focus-management.mdsrc/content/til/astro-content-collections-zod.mdsrc/content/til/cloudinary-image-presets.mdsrc/content/til/content-readability-optimization.mdsrc/content/til/convex-real-time-backend.mdsrc/content/til/notion-cms-astro-loader.mdsrc/content/til/opentelemetry-wide-events.mdsrc/content/til/react-email-templates.mdsrc/content/til/solid-primitives-storage.mdsrc/content/til/stripe-webhook-processing.mdsrc/content/til/suspense-error-boundaries.mdsrc/content/til/tanstack-query-server-state.mdsrc/content/til/temporal-api-date-handling.mdsrc/content/til/timezone-aware-cron-jobs.mdsrc/content/til/view-transitions-api.mdsrc/content/til/wcag-accessibility-audit.md
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Use Notion integration with vendored `notion-astro-loader` in `vendor/` for live content sync, requiring `NOTION_TOKEN` and `NOTION_RR_RESOURCES_ID` environment variables
Applied to files:
src/content/til/notion-cms-astro-loader.mdsrc/content/til/2025-08-13-notion-astro-loader.md
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Use Astro Content Collections with three sources: `post` (local MDX from `src/data/post/`), `til` (from `src/content/til/`), and `resources` (from Notion database via `notionLoader`)
Applied to files:
src/content/til/notion-cms-astro-loader.mdsrc/content/til/2025-07-08-til-content-collection.mdsrc/content/til/astro-content-collections-zod.mdsrc/content/til/2025-08-13-notion-astro-loader.md
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Enable server-side rendering via `astrojs/vercel` adapter and experimental `liveContentCollections` for Notion sync
Applied to files:
src/content/til/notion-cms-astro-loader.mdsrc/content/til/2025-07-08-til-content-collection.mdsrc/content/til/astro-content-collections-zod.mdsrc/content/til/2025-08-13-notion-astro-loader.md
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Applies to **/*.{astro,tsx,jsx} : Use `tailwind-variants` for component variant styling
Applied to files:
src/content/til/2024-12-30-astrowind-foundation.md
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Use Pagefind for client-side search functionality
Applied to files:
src/content/til/2025-05-18-pagefind-search.md
🪛 LanguageTool
src/content/til/tanstack-query-server-state.md
[grammar] ~11-~11: Use a hyphen to join words.
Context: ...tate I spent weeks building custom data fetching utilities before discovering th...
(QB_NEW_EN_HYPHEN)
[grammar] ~102-~102: Ensure spelling is correct
Context: ...ents(prevWeek), }); } }); ## placeholderData Prevents Loading Flashes typescript...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
src/content/til/2024-12-30-astrowind-foundation.md
[style] ~18-~18: This phrase is redundant (‘O’ stands for ‘optimization’). Use simply “SEO”.
Context: ...th dark mode support out of the box - SEO optimization built into the layout components - **...
(ACRONYM_TAUTOLOGY)
🔇 Additional comments (35)
src/content/til/suspense-error-boundaries.md (1)
1-184: Comprehensive TIL with sound architecture patterns.This is well-structured educational content that clearly articulates the problem (cascading failures), progressively builds to the solution (granular boundaries), and provides reusable patterns for implementation. The code examples effectively demonstrate framework-specific APIs (React Query, Solid.js) while keeping the patterns universal and applicable to both ecosystems.
Key strengths:
- Clear problem-to-solution progression
- Illustrative code samples that balance clarity with practical completeness
- Sound guidance on boundary placement (independent fetches, third-party components, user-generated content)
- Appropriate use of skeleton loaders and background refetch indicators
The simplified
retry()function (lines 67–70) appropriately omits boilerplate; readers can infer refetch logic from their query library's API.src/content/til/view-transitions-api.md (1)
128-131: LGTM—strong educational content.The Key Insight section effectively captures the core value proposition, and the article's progression from problem to solution is clear and practical. The examples teach the right mental model (view transitions as first-class UX rather than afterthought), which is the heart of good technical writing.
src/content/til/solid-primitives-storage.md (1)
1-170: Excellent TIL entry with clear narrative and practical examples.The documentation effectively contrasts custom vs. library-based approaches, progresses logically through features (SSR, sync, serialization, backends), and includes a sophisticated migration pattern. Code examples are accurate, well-formatted, and cover real-world scenarios like Date serialization. The conclusion ties insights together with actionable advice.
src/content/til/2025-10-09-url-sanitization-security.md (2)
35-44: Both fixes are secure and well-presented.The two code examples correctly address the vulnerability with appropriate levels of rigor. The first demonstrates proper URL parsing with hostname validation, and the second strengthens the approach by validating the full origin (including protocol) using a Set for better performance. Both include proper error handling with try/catch, which is important for rejecting malformed URLs. The progression from "a fix" to "better yet" helps readers understand security trade-offs.
Also applies to: 49-62
26-29: Attack vectors are accurate and well-chosen.The two examples (query parameter injection and subdomain spoofing) are realistic attack scenarios that clearly demonstrate why substring matching fails. This helps readers understand the severity and practical implications of the vulnerability.
src/content/til/2025-06-29-cache-control-strategy.md (1)
1-56: Excellent technical content with accurate caching strategy.The article correctly explains a nuanced three-tier cache-control strategy with accurate directives:
- API routes with
no-store, must-revalidateensure fresh data- Static assets with
max-age=31536000(1 year) andimmutableleverage long-term caching- HTML pages with
max-age=0ands-maxage=86400separate browser and CDN caching, withstale-while-revalidate=604800enabling background refreshesThe pattern ordering in the vercel.json config (specific to general) is correct. The explanation of the caching strategy and benefits is clear and helpful for readers. The YAML frontmatter is properly formatted.
src/content/til/opentelemetry-wide-events.md (1)
1-170: Excellent documentation on the wide events pattern.The TIL entry effectively teaches structured logging with OpenTelemetry through clear problem statement, practical examples, and real-world integration points (request handlers, tRPC middleware, analytics queries). The RequestEvent class design using fluent interfaces is well-chosen, and the SQL examples demonstrate concrete value. The progression from problem → solution → integration → querying is pedagogically sound.
src/content/til/convex-real-time-backend.md (2)
1-106: Well-structured educational documentation with clear narrative flow.The TIL entry effectively communicates the value proposition of Convex through a clear progression: problem statement (WebSocket complexity) → solution overview → practical code examples → type safety benefits → key insight. The writing is engaging and accessible for the target audience.
36-54: Convex API examples verified as current and accurate.The TypeScript code examples align with current Convex documentation (v1.18+). The query definition pattern,
v.string()validator,ctx.db.query()with filter/first methods, anduseQuery()hook usage are all consistent with documented best practices. The reactive update behavior is correctly described.src/content/til/2025-06-25-bot-protection.md (2)
1-7: LGTM on metadata.The YAML frontmatter is well-structured with proper date formatting, descriptive tags, and a clear description. The draft status is correctly set to false for publication.
9-27: LGTM on content structure and approach explanation.The problem statement is clear, and the explanation of Vercel's invisible bot protection approach is easy to follow. Good UX-focused framing.
src/content/til/2025-05-18-pagefind-search.md (1)
1-49: Well-structured documentation of Pagefind integration.The article clearly explains the benefits of Pagefind, provides correct installation and integration steps for Astro, and highlights a key advantage (build-time index synchronization). The code examples are accurate and easy to follow. Aligns well with project patterns.
src/content/til/2025-03-23-vercel-speed-insights.md (1)
1-41: Excellent explanation of RUM benefits and straightforward integration.The article effectively contrasts synthetic vs. real user metrics, provides a minimal and correct Astro integration example, and reinforces the value with a concrete real-world insight (mobile TTFB discovery). Clear takeaway about complementing synthetic tests with RUM.
src/content/til/react-email-templates.md (1)
1-184: Comprehensive React Email guide—excellent workflow documentation.This article effectively covers the full spectrum from component building through production email delivery. The progression from problem statement to reusable patterns to third-party integration (Resend) and webhook handling is logical and practical.
src/content/til/2025-05-27-react-email-welcome.md (1)
1-54: Excellent getting-started guide for React Email.This article provides a concise, accessible introduction to building emails with React Email. The simplicity is a strength here—clear code examples and a focus on the preview workflow lower the barrier to entry.
src/content/til/2025-08-15-internal-linking-seo.md (2)
1-7: File naming and frontmatter are correctly formatted.Filename prefix and date are consistent and follow the TIL collection convention.
20-41: Internal linking implementation is sound for documentation purposes.The
addInternalLinks()function correctly uses a regex with word boundaries and negative lookahead ((?![^<]*>)) to avoid linking terms already inside HTML tags. Limiting to first occurrence per term prevents over-optimization. This is a practical example for the article's scope.src/content/til/2025-08-13-notion-astro-loader.md (3)
1-7: File naming and frontmatter are correctly formatted.Filename prefix and date are consistent and follow the TIL collection convention.
17-42: Notion loader implementation is technically sound.The
notionLoader()function correctly uses the Notion client library, handles database queries with optional filtering, extracts page data, and populates the content store. The code aligns with Astro v5's loader API and the repository's learnings about the vendorednotion-astro-loader.
9-64: Consider repositioning these complementary TIL entries rather than consolidating them.While both files cover Notion and Astro content loaders, they serve different purposes:
2025-08-13-notion-astro-loader.mdfocuses on building and vendoring the custom loader implementation (internal architecture), whilenotion-cms-astro-loader.mdfocuses on using the loader as a CMS (configuration, property mapping, live sync, and image handling).Rather than merge them into one, consider:
- Linking them explicitly if both perspectives are valuable
- Clarifying in titles/descriptions which audience each targets (builder vs. user)
- Removing one only if the content is truly redundant
Likely an incorrect or invalid review comment.
src/content/til/notion-cms-astro-loader.md (1)
9-84: Content serves different purposes and should not be consolidated.While both articles use Notion with Astro, they target different audiences and cover distinct topics.
notion-cms-astro-loader.mdis a user-facing guide on configuring Notion as a CMS (covering property mapping, live sync, image handling, and rate-limit considerations), whereas2025-08-13-notion-astro-loader.mdis a developer-focused article on implementing the custom loader itself with vendoring strategy. The configuration patterns shown are necessarily similar because they describe the same integration, but the content scope and depth differ significantly. No consolidation needed.Likely an incorrect or invalid review comment.
src/content/til/2025-12-23-accordion-ssr-flash.md (1)
1-60: Overall structure and clarity are excellent.The documentation is well-organized with clear sections, good use of the problem-solution pattern, and accurate explanation of SSR hydration issues. The Astro component example is syntactically correct and demonstrates the proper SSR pattern using native
<details>and theopenattribute.src/content/til/timezone-aware-cron-jobs.md (1)
140-146: > Likely an incorrect or invalid review comment.src/content/til/2025-06-27-accessibility-testing-ci.md (1)
42-52: No issues found. The import path and AxeBuilder API usage are correct and align with the @axe-core/playwright documentation.src/content/til/wcag-accessibility-audit.md (1)
17-27: The bash commands are correct. Bothnpx @axe-core/cli https://localhost:3000andnpx lighthouse https://localhost:3000 --only-categories=accessibilityuse valid syntax and supported flags.src/content/til/accessibility-testing-pipeline-axe.md (1)
51-76: Solid test script example with proper async patterns.The AxePuppeteer integration and browser lifecycle management are correct. The script clearly demonstrates how to collect violations and passes across multiple URLs.
src/content/til/2025-10-05-starwind-pagination-breadcrumbs.md (2)
25-43: Clear Astro component usage example with strong accessibility focus.The pagination example properly demonstrates ARIA labeling and the props interface. The implicit handling of disabled states and aria-current is well-suited for a TIL entry.
47-62: Breadcrumbs example follows good accessibility patterns.The children-based composition with proper
navlandmark and aria-label demonstrates the correct mental model. The mention of schema.org support is a nice detail for SEO-conscious developers.src/content/til/accessibility-ci-cd-pipeline.md (2)
32-46: Robust server startup with proper error handling and timeout logic.The wait loop is well-designed with clear success/failure paths and a reasonable timeout (60 seconds). One minor note: the PID captured in line 35 isn't used in subsequent steps; you could remove it if this is simplifying the example.
93-101: Mobile testing command is accurate and well-configured.The form-factor and screen emulation settings are realistic and the JSON output format is appropriate for CI/CD pipelines.
src/content/til/2025-07-08-til-content-collection.md (1)
1-7: Frontmatter and metadata are solid.The YAML frontmatter is well-formed with consistent date formatting between filename and date field. Tags are appropriate and description is concise. The
draft: falseflag is correct for publication.src/content/til/astro-content-collections-zod.md (2)
43-53: Type inference demonstration is clear and correct.The getCollection example effectively shows how TypeScript automatically infers types from the Zod schema. The comments on lines 50–51 clearly communicate the type-safety benefit, making this section particularly valuable for readers new to schema-driven type systems.
72-89: Shared schema pattern demonstrates good DX practices.The
metadataDefinition()helper is a solid pattern for extracting reusable Zod schema fragments. This example teaches an important principle: composable, reusable schema definitions reduce duplication and improve maintainability across multiple content collections.src/content/til/tanstack-query-server-state.md (2)
1-114: Excellent educational content; solid TIL entry.The article effectively explains the distinction between server state and client state, then demonstrates TanStack Query patterns with progressively advanced examples (custom hook → centralized keys → prefetching → placeholder data). Code snippets are clear and the narrative flows well. This fits the TIL collection nicely.
39-48: All API usages are current and correct for TanStack Query v5/v6 with SolidJS. The examples accurately demonstrate the current patterns:
createQuery(() => ({ ... }))thunk pattern is correct for reactive inputsplaceholderDatasupports both direct values (line 46) and callback functions (line 108)queryClient.invalidateQueries({ queryKey: ... })andqueryClient.prefetchQuery({ queryKey, queryFn })follow the standard object-style options APINo changes needed.
| export async function POST({ request }) { | ||
| const botVerification = request.headers.get('x-vercel-bot-protection'); | ||
|
|
||
| if (botVerification !== 'verified') { | ||
| return new Response('Bot detected', { status: 403 }); | ||
| } | ||
|
|
||
| // Process legitimate submission | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Vercel Bot Protection header name and value verification API documentation
💡 Result:
Short answer: the header is named x-is-human. Its value is the BotID token produced by Vercel’s client-side BotID script; on the server you verify it using Vercel’s BotID verification API (e.g., checkBotId / botid server helpers or withBotId middleware). [1][2]
Sources
- Vercel BotID docs (get‑started + server check example). [1]
- Vercel community thread showing the x-is-human header used by BotID. [2]
Correct the bot protection header and verification method to match current Vercel BotID API.
The code example uses incorrect header information. The correct header is x-is-human (not x-vercel-bot-protection), and its value is a BotID token that must be verified using Vercel's BotID verification API—either through helper functions like checkBotId/botid or middleware like withBotId. Simply checking for a "verified" string is not the correct implementation.
🤖 Prompt for AI Agents
In @src/content/til/2025-06-25-bot-protection.md around lines 49 - 57, The POST
handler currently reads the wrong header and treats it as a simple "verified"
string; change it to read the BotID token from the x-is-human header and
validate it using Vercel's BotID verification utilities (e.g., call checkBotId
or botid verifier inside POST, or wrap the route with withBotId middleware)
instead of string comparison; if verification fails, return a 403 Response,
otherwise continue to process the legitimate submission.
| ```typescript | ||
| // Only enhance, don't change initial state | ||
| document.querySelectorAll('[data-accordion-item]').forEach(item => { | ||
| item.addEventListener('click', (e) => { | ||
| // Add smooth height animation | ||
| // Don't toggle open/closed - let browser handle that | ||
| }); | ||
| }); | ||
| ``` |
There was a problem hiding this comment.
Complete the progressive enhancement script example.
The JavaScript enhancement script is incomplete. It attaches a click handler but provides no actual implementation beyond a TODO comment about smooth height animation. This leaves readers unclear about how to properly enhance the accordion.
🔧 Proposed enhancement
// Only enhance, don't change initial state
document.querySelectorAll('[data-accordion-item]').forEach(item => {
- item.addEventListener('click', (e) => {
- // Add smooth height animation
- // Don't toggle open/closed - let browser handle that
- });
+ const summary = item.querySelector('summary');
+ const content = item.querySelector('.content');
+
+ summary?.addEventListener('click', (e) => {
+ // Use CSS transition for smooth animation
+ if (!item.style.transition) {
+ item.style.transition = 'all 0.3s ease-in-out';
+ }
+ });
});Alternatively, for a more robust approach using the Web Animations API or CSS custom properties for max-height animation:
summary?.addEventListener('click', () => {
requestAnimationFrame(() => {
const isOpen = item.hasAttribute('open');
if (isOpen) {
item.style.maxHeight = content.scrollHeight + 'px';
}
});
});| ## The React Form Component | ||
|
|
||
| ```tsx | ||
| // src/components/ContactForm.tsx | ||
| import { useActionState } from 'react'; | ||
| import { actions } from 'astro:actions'; | ||
|
|
||
| export function ContactForm() { | ||
| const [state, action, pending] = useActionState( | ||
| actions.submitContact, | ||
| { success: false, errors: {} } | ||
| ); | ||
|
|
||
| return ( | ||
| <form action={action}> | ||
| <input name="name" disabled={pending} /> | ||
| {state.errors?.name && <span>{state.errors.name}</span>} | ||
|
|
||
| <button type="submit" disabled={pending}> | ||
| {pending ? 'Sending...' : 'Send Message'} | ||
| </button> | ||
| </form> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/content/til/2025-12-23-astro-actions-react-forms.mdRepository: Resonant-Projects/rproj-website
Length of output: 2658
🌐 Web query:
Astro Actions validation error handling useActionState state.errors
💡 Result:
Short answer: useActionState returns a state object whose error is an ActionError (not a plain "errors" map). For validation details you should check state.error with Astro’s isInputError() and then read error.fields for per-field messages — map those into your component form errors (e.g. local state keyed by input name). See the Actions docs and the isInputError guidance.
Example pattern (concise):
- Initialize useActionState with an initial state that includes an error slot.
- After an action runs, if state.error exists and isInputError(state.error) is true, read state.error.fields to populate your field-level errors.
Example (React client component):
- import { useActionState, withState } from 'react'; import { actions, isInputError } from 'astro:actions';
- const [state, action] = useActionState(withState(actions.signup), { data: null, error: undefined });
- // In render: if (state.error && isInputError(state.error)) show state.error.fields.email?.join(', ')
Docs:
- Actions guide (validation, isInputError, error.fields). [1]
- Actions API / isInputError reference. [2]
- useActionState / withState integration example. [3]
Sources:
[1] Astro Actions guide — Displaying form input errors.
[2] Astro Actions API reference — isInputError / error shape.
[3] @astrojs/react docs — useActionState + withState example.
Incorrect error handling and incomplete form example.
The form example demonstrates the wrong pattern for handling Astro Actions validation errors. Astro Actions returns errors via state.error (singular) as an ActionError object, not state.errors (plural). To display per-field errors, you must:
- Check
isInputError(state.error)to verify it's a validation error - Access field errors from
state.error.fields(e.g.,state.error.fields.name) - Wrap the action with
withState()when passing touseActionState
The current example also omits the email and message fields (only shows name), uses incorrect state shape, and lacks the isInputError check. Developers copying this code will have non-functional error display.
Required fixes:
- Add the missing email and message input fields
- Replace
state.errors?.namewith proper error extraction fromstate.error.fieldsafterisInputError()check - Wrap action:
useActionState(withState(actions.submitContact), ...) - Update initial state to match Astro Actions pattern:
{ data: null, error: undefined } - Import and demonstrate
isInputErrorusage
🤖 Prompt for AI Agents
In @src/content/til/2025-12-23-astro-actions-react-forms.md around lines 38 -
62, The ContactForm example uses the wrong state shape and error handling;
update it to call useActionState(withState(actions.submitContact), { data: null,
error: undefined }) (wrap the action with withState), import and use
isInputError to check state.error before reading validation details, and replace
state.errors?.name with state.error.fields.name (and similarly for email and
message) after the isInputError guard; also add the missing email and message
inputs and display their field errors using state.error.fields.<fieldName>.
| ```javascript | ||
| class FocusManager { | ||
| constructor() { | ||
| this.modalStack = []; | ||
| this.lastFocus = null; | ||
| } | ||
|
|
||
| trapFocus(container) { | ||
| this.lastFocus = document.activeElement; | ||
| this.modalStack.push(container); | ||
|
|
||
| const focusables = this.getFocusableElements(container); | ||
| if (focusables.length) focusables[0].focus(); | ||
|
|
||
| container.addEventListener('keydown', this.handleTabKey); | ||
| } | ||
|
|
||
| releaseFocus() { | ||
| const container = this.modalStack.pop(); | ||
| container?.removeEventListener('keydown', this.handleTabKey); | ||
| this.lastFocus?.focus(); | ||
| } | ||
|
|
||
| handleTabKey = (e) => { | ||
| if (e.key !== 'Tab') return; | ||
|
|
||
| const container = this.modalStack.at(-1); | ||
| const focusables = this.getFocusableElements(container); | ||
| const first = focusables[0]; | ||
| const last = focusables.at(-1); | ||
|
|
||
| if (e.shiftKey && document.activeElement === first) { | ||
| e.preventDefault(); | ||
| last.focus(); | ||
| } else if (!e.shiftKey && document.activeElement === last) { | ||
| e.preventDefault(); | ||
| first.focus(); | ||
| } | ||
| }; | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Define the missing getFocusableElements method.
The FocusManager class references this.getFocusableElements() at lines 28 and 44, but this method is never defined in the provided code. Add it to the class to make the example complete:
getFocusableElements(container) {
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
return Array.from(container.querySelectorAll(focusableSelectors));
}Alternatively, clarify in the documentation that this is a utility method readers need to implement based on their project's focusable elements.
🤖 Prompt for AI Agents
In @src/content/til/advanced-focus-management.md around lines 17 - 57, Add a
getFocusableElements(container) method to the FocusManager class (used by
trapFocus and handleTabKey) that accepts a container element, queries for common
focusable selectors (e.g., button, [href], input, select, textarea,
[tabindex]:not([tabindex="-1"])), returns an Array of elements, and gracefully
handles a null/undefined container by returning an empty array so trapFocus,
handleTabKey, and releaseFocus work reliably with the modalStack.
| const observer = new MutationObserver(mutations => { | ||
| mutations.forEach(mutation => { | ||
| mutation.addedNodes.forEach(node => { | ||
| if (node.hasAttribute?.('data-auto-focus')) { | ||
| const target = node.querySelector('[data-focus-target]'); | ||
| target?.focus(); | ||
| announce(node.dataset.announce); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| ``` |
There was a problem hiding this comment.
Define the announce() function or document its dependency.
Line 130 calls announce(node.dataset.announce), but this function is undefined. Either provide its implementation or clarify that it's a custom utility function (e.g., for screen reader announcements via aria-live regions). For completeness, consider adding a minimal example:
Example implementation for screen reader announcements
function announce(message) {
const live = document.querySelector('[aria-live="polite"]') ||
document.createElement('div');
live.setAttribute('aria-live', 'polite');
live.textContent = message;
if (!document.body.contains(live)) document.body.appendChild(live);
}🤖 Prompt for AI Agents
In @src/content/til/advanced-focus-management.md around lines 124 - 135, The
code calls announce(node.dataset.announce) inside the MutationObserver callback
but announce is undefined; either add a minimal accessible announcement utility
named announce that accepts a string and posts it to an aria-live region (create
or select an element with aria-live="polite"/"assertive", set its textContent,
append if missing) or document that announce is an external dependency and
import it; update the file to include that announce function (used by
observer/MutationObserver and reading node.dataset.announce) or add an explicit
import statement and a short comment pointing to the utility.
| export function analyzeReadability(text) { | ||
| const sentences = text.match(/[^.!?]+[.!?]+/g) || []; | ||
| const words = text.split(/\s+/).filter(Boolean); | ||
| const syllables = words.reduce((sum, word) => sum + countSyllables(word), 0); | ||
|
|
||
| const avgSentenceLength = words.length / sentences.length; | ||
| const avgSyllablesPerWord = syllables / words.length; | ||
|
|
||
| // Flesch Reading Ease formula | ||
| const flesch = 206.835 - (1.015 * avgSentenceLength) - (84.6 * avgSyllablesPerWord); | ||
|
|
||
| return { | ||
| flesch: Math.round(flesch), | ||
| avgSentenceLength: Math.round(avgSentenceLength * 10) / 10, | ||
| totalWords: words.length, | ||
| totalSentences: sentences.length, | ||
| grade: fleschToGrade(flesch), | ||
| }; | ||
| } |
There was a problem hiding this comment.
Define the missing countSyllables helper function.
The analyzeReadability function calls countSyllables(word) on line 33, but this helper is never defined or imported. The code would fail at runtime.
🔧 Proposed fix
Add the missing helper function at the top of the file:
export function analyzeReadability(text) {
+ // Helper: Estimate syllable count (simple heuristic)
+ function countSyllables(word) {
+ word = word.toLowerCase();
+ let count = 0;
+ const vowels = 'aeiouy';
+ let previousWasVowel = false;
+
+ for (let i = 0; i < word.length; i++) {
+ const isVowel = vowels.includes(word[i]);
+ if (isVowel && !previousWasVowel) {
+ count++;
+ }
+ previousWasVowel = isVowel;
+ }
+
+ // Adjust for silent 'e'
+ if (word.endsWith('e')) {
+ count--;
+ }
+
+ return Math.max(1, count);
+ }
+
const sentences = text.match(/[^.!?]+[.!?]+/g) || [];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function analyzeReadability(text) { | |
| const sentences = text.match(/[^.!?]+[.!?]+/g) || []; | |
| const words = text.split(/\s+/).filter(Boolean); | |
| const syllables = words.reduce((sum, word) => sum + countSyllables(word), 0); | |
| const avgSentenceLength = words.length / sentences.length; | |
| const avgSyllablesPerWord = syllables / words.length; | |
| // Flesch Reading Ease formula | |
| const flesch = 206.835 - (1.015 * avgSentenceLength) - (84.6 * avgSyllablesPerWord); | |
| return { | |
| flesch: Math.round(flesch), | |
| avgSentenceLength: Math.round(avgSentenceLength * 10) / 10, | |
| totalWords: words.length, | |
| totalSentences: sentences.length, | |
| grade: fleschToGrade(flesch), | |
| }; | |
| } | |
| export function analyzeReadability(text) { | |
| // Helper: Estimate syllable count (simple heuristic) | |
| function countSyllables(word) { | |
| word = word.toLowerCase(); | |
| let count = 0; | |
| const vowels = 'aeiouy'; | |
| let previousWasVowel = false; | |
| for (let i = 0; i < word.length; i++) { | |
| const isVowel = vowels.includes(word[i]); | |
| if (isVowel && !previousWasVowel) { | |
| count++; | |
| } | |
| previousWasVowel = isVowel; | |
| } | |
| // Adjust for silent 'e' | |
| if (word.endsWith('e')) { | |
| count--; | |
| } | |
| return Math.max(1, count); | |
| } | |
| const sentences = text.match(/[^.!?]+[.!?]+/g) || []; | |
| const words = text.split(/\s+/).filter(Boolean); | |
| const syllables = words.reduce((sum, word) => sum + countSyllables(word), 0); | |
| const avgSentenceLength = words.length / sentences.length; | |
| const avgSyllablesPerWord = syllables / words.length; | |
| // Flesch Reading Ease formula | |
| const flesch = 206.835 - (1.015 * avgSentenceLength) - (84.6 * avgSyllablesPerWord); | |
| return { | |
| flesch: Math.round(flesch), | |
| avgSentenceLength: Math.round(avgSentenceLength * 10) / 10, | |
| totalWords: words.length, | |
| totalSentences: sentences.length, | |
| grade: fleschToGrade(flesch), | |
| }; | |
| } |
🤖 Prompt for AI Agents
In @src/content/til/content-readability-optimization.md around lines 30 - 48,
analyzeReadability calls an undefined helper countSyllables, causing runtime
failure; add a countSyllables(word) function above analyzeReadability that
implements a simple heuristic (lowercase the word, strip non-letters, count
vowel groups as syllables, subtract for trailing silent 'e' and common
diphthongs, and ensure a minimum of 1 syllable) so syllable totals and flesch
calculation work correctly; ensure the function name is exactly countSyllables
and export it if the module pattern requires it so analyzeReadability can call
it.
| async function auditContent() { | ||
| const files = await glob('src/content/**/*.md'); | ||
| const results = []; | ||
|
|
||
| for (const file of files) { | ||
| const content = await readFile(file, 'utf-8'); | ||
| const { content: body } = matter(content); | ||
| const analysis = analyzeReadability(body); | ||
|
|
||
| results.push({ | ||
| file, | ||
| ...analysis, | ||
| passesTargets: analysis.flesch >= 60 && analysis.avgSentenceLength <= 20, | ||
| }); | ||
| } | ||
|
|
||
| return results; | ||
| } |
There was a problem hiding this comment.
Add missing import statement for analyzeReadability.
The auditContent function on line 112 calls analyzeReadability(body), but the function is not imported in this code block. Add the import at the top of the script.
🔧 Proposed fix
// scripts/content-analyzer.js
import { glob } from 'glob';
import { readFile } from 'fs/promises';
import matter from 'gray-matter';
+import { analyzeReadability } from '../src/utils/readability-analyzer.js';
async function auditContent() {Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @src/content/til/content-readability-optimization.md around lines 105 - 122,
The auditContent function calls analyzeReadability(body) but analyzeReadability
is not imported; add an import for the analyzeReadability function at the top of
this script (the same module that defines analyzeReadability in the codebase) so
auditContent can call it—ensure the import uses the correct exported name
(analyzeReadability) and path to the module that implements it.
| export function suggestSimplifications(text) { | ||
| return Object.entries(simplifications) | ||
| .filter(([complex]) => text.toLowerCase().includes(complex)) | ||
| .map(([complex, simple]) => ({ | ||
| find: complex, | ||
| replace: simple, | ||
| })); | ||
| } |
There was a problem hiding this comment.
The suggestSimplifications function has a flawed regex for detecting words.
The function filters by checking text.toLowerCase().includes(complex), which matches substrings, not word boundaries. For example, "utilize" would match "utilizeability" or any word containing "utilize", leading to false positives.
🔧 Proposed fix
export function suggestSimplifications(text) {
return Object.entries(simplifications)
- .filter(([complex]) => text.toLowerCase().includes(complex))
+ .filter(([complex]) => new RegExp(`\\b${complex}\\b`, 'i').test(text))
.map(([complex, simple]) => ({
find: complex,
replace: simple,
}));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function suggestSimplifications(text) { | |
| return Object.entries(simplifications) | |
| .filter(([complex]) => text.toLowerCase().includes(complex)) | |
| .map(([complex, simple]) => ({ | |
| find: complex, | |
| replace: simple, | |
| })); | |
| } | |
| export function suggestSimplifications(text) { | |
| return Object.entries(simplifications) | |
| .filter(([complex]) => new RegExp(`\\b${complex}\\b`, 'i').test(text)) | |
| .map(([complex, simple]) => ({ | |
| find: complex, | |
| replace: simple, | |
| })); | |
| } |
🤖 Prompt for AI Agents
In @src/content/til/content-readability-optimization.md around lines 139 - 146,
The filter in suggestSimplifications incorrectly uses
text.toLowerCase().includes(complex) which matches substrings and causes false
positives; change it to test whole-word matches by creating a case-insensitive
regex with word boundaries for each key in simplifications (e.g., new
RegExp('\\b' + escapeRegExp(complex) + '\\b', 'i')), add an escapeRegExp helper
to safely escape special regex chars, and use regex.test(text) instead of
includes while keeping the rest of the mapping the same.
| ```typescript | ||
| // convex/stripe.ts | ||
| export const handleWebhook = internalAction({ | ||
| args: { event: v.any() }, | ||
| handler: async (ctx, { event }) => { | ||
| // Check if we've already processed this event | ||
| const existing = await ctx.runQuery(internal.stripe.getProcessedEvent, { | ||
| eventId: event.id, | ||
| }); | ||
|
|
||
| if (existing) { | ||
| console.log(`Event ${event.id} already processed, skipping`); | ||
| return; | ||
| } | ||
|
|
||
| // Process the event | ||
| await processEvent(ctx, event); | ||
|
|
||
| // Mark as processed | ||
| await ctx.runMutation(internal.stripe.markEventProcessed, { | ||
| eventId: event.id, | ||
| processedAt: Date.now(), | ||
| }); | ||
| }, | ||
| }); | ||
| ``` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "stripe-webhook-processing.md"Repository: Resonant-Projects/rproj-website
Length of output: 119
🏁 Script executed:
# Check if file exists and examine its full content
git ls-files | grep -i stripeRepository: Resonant-Projects/rproj-website
Length of output: 119
🏁 Script executed:
# Search for Convex-related files to understand the project structure
git ls-files | grep -E "(convex|package\.json)" | head -20Repository: Resonant-Projects/rproj-website
Length of output: 131
🏁 Script executed:
cat -n src/content/til/stripe-webhook-processing.mdRepository: Resonant-Projects/rproj-website
Length of output: 4690
🌐 Web query:
Convex framework atomic operations transactions database mutations
💡 Result:
Summary — how Convex handles atomic operations, transactions, and mutations:
- Mutations are transactional: all reads see a consistent snapshot and all writes commit together; if a mutation throws, no writes are persisted. [1][4]
- Convex implements optimistic concurrency control (OCC) with automatic rerun on conflicts, providing serializable (not just snapshot) isolation so you can code “the obvious way” without locks. [2][3]
- Mutations must be deterministic and cannot call external third‑party APIs (use Actions for external IO). [1]
- Client‑initiated mutations run in a single ordered queue per client; the runtime ensures atomicity and reactivity for queries/aggregates updated by those writes. [1][5]
Sources: Convex docs — Mutations & Transactions; OCC/atomicity docs; architecture overview. [1][2][3][4][5]
References:
[1] Mutations / Transactions (Convex docs). (docs.convex.dev)
[2] OCC and Atomicity (Convex docs). (docs.convex.dev)
[3] How Convex Works — Transactions (Convex docs/stack). (stack.convex.dev)
[4] Writing Data / db.insert, patch, etc. (Convex docs). (docs.convex.dev)
[5] Aggregates / transactional reactivity (npm/Convex aggregate). (npmjs.com)
Would you like example mutation code showing an atomic read-modify-write pattern?
Citations:
- 1: https://docs.convex.dev/functions/mutation-functions?utm_source=openai
- 2: https://docs.convex.dev/database/advanced/occ?utm_source=openai
- 3: https://stack.convex.dev/how-convex-works?utm_source=openai
- 4: https://docs.convex.dev/database/writing-data?utm_source=openai
- 5: https://www.npmjs.com/package/%40convex-dev/aggregate/v/0.1.8?utm_source=openai
🌐 Web query:
Convex conditional mutation atomicity idempotent webhooks
💡 Result:
Short answer + pattern:
-
Convex mutations are transactional and atomic — any reads/writes inside a mutation run in one transaction. Use mutations for conditional updates so the check-and-write is atomic. [1][2]
-
Triggers (and many Convex helpers) run inside the same mutation and therefore are atomic with the write that fired them. [3]
-
Convex’s client WebSocket mutation calls include built-in idempotency (client generates a unique mutation id and the server deduplicates retries), but HTTP actions (used to receive webhooks) do not get automatic retries/idempotency from Convex. HTTP actions should be treated as ordinary HTTP endpoints. [4][5]
Recommended webhook-safe pattern (concise):
- Receive webhook in an HTTP action (convex/http.ts). Parse the payload and extract an idempotency key (e.g., provider-sent delivery id or compute a stable hash). [4][5]
- Call a Convex mutation that, inside one transaction:
- checks a “processed_webhooks” table for the idempotency key,
- if missing, performs the side-effect(s) and inserts the key (or marks processed),
- otherwise returns early.
This makes processing conditional, atomic, and safe to retry. [1][2][4]
- Return a 2xx only after the mutation completes; otherwise return an error to prompt sender retries (if desired).
References:
- Convex — Mutations are transactional. [2]
- Convex — Mutations docs / transactionality and mutation examples. [8]
- Convex — Triggers run inside the same mutation (atomic). [0]
- Convex — HTTP actions (use for receiving webhooks; Convex won’t auto-retry). [1]
- Example/recipe: receiving GitHub webhooks into a Convex HTTP action. [4]
If you want, I can draft the exact HTTP action + mutation TypeScript code for your webhook provider and idempotency scheme.
Move idempotency check and processing into a single Convex mutation for atomicity.
The pattern shown has a real TOCTOU race condition: the idempotency check (ctx.runQuery()) runs outside the mutation, so between checking that an event wasn't processed and marking it processed, another webhook could process the same event.
In Convex, mutations are fully transactional—consolidate the check and write into a single mutation so the entire read-modify-write is atomic:
Example: atomic idempotent webhook processing
// Inside a Convex mutation (not action)
export const processWebhookEvent = mutation({
args: { event: v.any() },
handler: async (ctx, { event }) => {
// Check and write happen atomically in one transaction
const existing = await ctx.db
.query('processedEvents')
.withIndex('by_eventId', q => q.eq('eventId', event.id))
.first();
if (existing) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
// Process the event (can call internal queries/mutations here)
await processEvent(ctx, event);
// Mark as processed in same transaction
await ctx.db.insert('processedEvents', {
eventId: event.id,
processedAt: Date.now(),
});
},
});Alternatively, use a unique index on eventId with an upsert-style write to let the database enforce idempotency, or store a processed flag and query it conditionally.
🤖 Prompt for AI Agents
In @src/content/til/stripe-webhook-processing.md around lines 35 - 60, The
current handleWebhook action performs the idempotency check via
internal.stripe.getProcessedEvent and the write via
internal.stripe.markEventProcessed outside a single transaction, causing a
TOCTOU race; fix this by creating a Convex mutation (e.g., processWebhookEvent)
that takes the event, performs the existing check and the mark-as-processed
insert/update atomically using ctx.db (or a unique index/upsert on eventId), and
calls processEvent from inside that mutation; then change handleWebhook to
simply invoke that mutation with the event so the read-modify-write happens
inside one transactional mutation.
| ```typescript | ||
| // src/routes/checkout/success.tsx | ||
| export default function CheckoutSuccess() { | ||
| const [searchParams] = useSearchParams(); | ||
| const sessionId = searchParams.get('session_id'); | ||
|
|
||
| const syncStatus = createQuery(() => ({ | ||
| queryKey: ['checkout', 'sync', sessionId], | ||
| queryFn: async () => { | ||
| // Poll until subscription is synced | ||
| const maxAttempts = 10; | ||
| for (let i = 0; i < maxAttempts; i++) { | ||
| const sub = await checkSubscriptionStatus(sessionId); | ||
| if (sub.status === 'active') return sub; | ||
| await delay(1000); | ||
| } | ||
| throw new Error('Subscription sync timeout'); | ||
| }, | ||
| retry: false, | ||
| })); | ||
|
|
||
| return ( | ||
| <Show when={syncStatus.isSuccess} fallback={<SyncingIndicator />}> | ||
| <SuccessMessage subscription={syncStatus.data} /> | ||
| </Show> | ||
| ); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Framework mismatch: code example mixes React and SolidJS patterns.
The checkout success component uses SolidJS patterns (<Show> component, createQuery from TanStack Query SolidJS adapter) but the file path and imports suggest React conventions. The useSearchParams from React Router API also doesn't align with SolidJS usage.
This inconsistency will confuse readers trying to adapt the code. Either clarify the framework (add a comment like // SolidJS example), or rewrite using pure React patterns (useSearchParams() from React Router, conditional rendering with && or ternaries, useQuery from TanStack Query).
🔄 Example React version (if intending React)
-export default function CheckoutSuccess() {
- const [searchParams] = useSearchParams();
+export default function CheckoutSuccess() {
+ const [searchParams] = useSearchParams();
const sessionId = searchParams.get('session_id');
- const syncStatus = createQuery(() => ({
+ const syncStatus = useQuery({
queryKey: ['checkout', 'sync', sessionId],
queryFn: async () => {
const maxAttempts = 10;
for (let i = 0; i < maxAttempts; i++) {
const sub = await checkSubscriptionStatus(sessionId);
if (sub.status === 'active') return sub;
await delay(1000);
}
throw new Error('Subscription sync timeout');
},
- retry: false,
- }));
+ retry: false,
+ });
return (
- <Show when={syncStatus.isSuccess} fallback={<SyncingIndicator />}>
- <SuccessMessage subscription={syncStatus.data} />
- </Show>
+ {syncStatus.isSuccess ? (
+ <SuccessMessage subscription={syncStatus.data} />
+ ) : (
+ <SyncingIndicator />
+ )}
);
}🤖 Prompt for AI Agents
In @src/content/til/stripe-webhook-processing.md around lines 99 - 126, The
example mixes SolidJS and React patterns; either mark it explicitly as SolidJS
or convert it to React. If keeping SolidJS, add a top-line comment like "//
SolidJS example" and ensure imports/use of createQuery, Show, and
useSearchParams are from SolidJS/TanStack Solid adapters; keep CheckoutSuccess,
createQuery, Show, checkSubscriptionStatus, delay, SyncingIndicator, and
SuccessMessage as-is. If converting to React, replace createQuery with useQuery
from TanStack React, replace Solid's useSearchParams and Show usage with React
Router's useSearchParams and conditional rendering (ternary or &&) around
SuccessMessage, and keep polling logic using checkSubscriptionStatus and delay
but inside the useQuery queryFn; update imports accordingly.
- Updated the description to fix a grammatical error, ensuring clarity and professionalism in the content.
- Removed unnecessary blank lines and adjusted formatting for improved readability across multiple "Today I Learned" articles. - Ensured consistent code style and presentation in entries related to AstroWind, Vercel Speed Insights, Pagefind, and others. need pre-commit hooks :(
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In @src/content/til/2024-12-30-astrowind-foundation.md:
- Around line 26-32: The code block is labeled as TypeScript but contains CSS
(the :root selector and CSS variables like --aw-color-primary and
--aw-color-secondary); update the code fence label from "typescript" to "css" so
the snippet is correctly highlighted and reflects the actual language.
In @src/content/til/2025-06-25-cloudinary-integration.md:
- Around line 24-35: The code uses the fill() resize action but doesn't import
it, causing a runtime/compile error; update the Cloudinary module by importing
the resize action (e.g., import { fill } from
'@cloudinary/url-gen/actions/resize') and keep the existing Cloudinary import
and getOptimizedUrl implementation so
cld.image(publicId).format('auto').quality('auto').resize(fill().width(width)).toURL()
can call fill() successfully.
In @src/content/til/notion-cms-astro-loader.md:
- Around line 1-7: The file name doesn't follow the TIL date-prefix convention;
rename the file to match its frontmatter date by using git mv to change
src/content/til/notion-cms-astro-loader.md to
2025-01-03-notion-cms-astro-loader.md (or the same directory path with that
prefix), update any internal references or index entries that point to the old
filename, and commit so history is preserved.
In @src/content/til/opentelemetry-wide-events.md:
- Around line 125-142: The POST handler currently calls req.json(), uses
process.env.AXIOM_TOKEN, and forwards with fetch without any error handling or
validation; update the POST function to wrap req.json() in a try/catch to return
a 400 on malformed JSON, check that process.env.AXIOM_TOKEN is present and
return a 500/422 with a clear message if missing, and wrap the fetch call in
try/catch then validate the response status (treat non-2xx as an error),
returning an appropriate error response and logging details; keep these changes
scoped to the POST function and reference req.json(), process.env.AXIOM_TOKEN,
and the fetch to 'https://api.axiom.co/v1/datasets/events/ingest'.
In @src/content/til/timezone-aware-cron-jobs.md:
- Around line 29-37: The current export uses invalid object-literal Convex
syntax (cronJobs = cronJobs({...})) and passes a raw function; replace this with
a crons instance registration using the instance methods and a
FunctionReference: create a crons instance and register the job with
crons.cron(...) or crons.hourly('dailyEmailDispatch',
internal.emails.dailyEmailHandler) (or crons.cron('dailyEmailDispatch', '0 * * *
*', internal.emails.dailyEmailHandler)), ensure the handler is a
FunctionReference like internal.emails.dailyEmailHandler rather than
dailyEmailHandler, and export the instance as export default crons.
🧹 Nitpick comments (4)
src/content/til/2024-12-30-astrowind-foundation.md (1)
19-19: Fix redundant phrase "SEO optimization".AstroWind provides SEO optimization built into the layout components, but "SEO optimization" is redundant. Use simply "SEO" instead, as the 'O' already stands for "optimization."
✏️ Proposed fix
- **SEO optimization** built into the layout components + **SEO support** built into the layout componentssrc/content/til/tanstack-query-server-state.md (1)
11-11: Use hyphenated compound modifier.When using "data fetching" as a compound adjective before a noun, use a hyphen: "data-fetching utilities."
✏️ Proposed fix
-I spent weeks building custom data fetching utilities before discovering that TanStack Query handles every edge case I hadn't even considered. +I spent weeks building custom data-fetching utilities before discovering that TanStack Query handles every edge case I hadn't even considered.src/content/til/accessibility-testing-pipeline-axe.md (2)
75-89: Add explicit health check before running accessibility tests.Line 82 runs the preview server in the background without verifying it's ready, which can cause intermittent CI/CD failures if tests start before the server is accessible.
✅ Proposed workflow improvement
- run: npm run build - run: npm run preview & - - run: sleep 5 + - run: node -e "const http = require('http'); const check = () => http.get('http://localhost:4321', {timeout: 5000}, () => process.exit(0)).on('error', () => {setTimeout(check, 500)}); check();" - run: npm run axe:scanOr use a dedicated health-check script/tool like
wait-on:- run: npm run preview & - - run: sleep 5 + - run: npx wait-on http://localhost:4321 --timeout 30000 - run: npm run axe:scan
46-71: Add error handling and resource cleanup to test script.The test script lacks error handling. If any URL processing fails,
browser.close()won't be called, leaking resources. Additionally, individual URL failures will cause the entire test to abort.🛡️ Proposed error handling improvement
async function runA11yTests(urls) { const browser = await puppeteer.launch({ headless: true }); const results = []; - - for (const url of urls) { - const page = await browser.newPage(); - await page.goto(`http://localhost:4321${url}`); - - const axeResults = await new AxePuppeteer(page).analyze(); - - results.push({ - url, - violations: axeResults.violations, - passes: axeResults.passes.length, - }); - } - - await browser.close(); + + try { + for (const url of urls) { + try { + const page = await browser.newPage(); + await page.goto(`http://localhost:4321${url}`, { waitUntil: 'networkidle2' }); + + const axeResults = await new AxePuppeteer(page).analyze(); + + results.push({ + url, + violations: axeResults.violations, + passes: axeResults.passes.length, + }); + + await page.close(); + } catch (urlError) { + console.error(`Failed to test ${url}:`, urlError.message); + results.push({ + url, + error: urlError.message, + violations: [], + }); + } + } + } finally { + await browser.close(); + } + return results; }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
src/content/til/2024-12-30-astrowind-foundation.mdsrc/content/til/2025-03-23-vercel-speed-insights.mdsrc/content/til/2025-05-18-pagefind-search.mdsrc/content/til/2025-05-27-notion-contact-form.mdsrc/content/til/2025-05-27-react-email-welcome.mdsrc/content/til/2025-06-25-bot-protection.mdsrc/content/til/2025-06-25-cloudinary-integration.mdsrc/content/til/2025-06-29-cache-control-strategy.mdsrc/content/til/2025-07-08-til-content-collection.mdsrc/content/til/2025-08-13-notion-astro-loader.mdsrc/content/til/2025-08-15-internal-linking-seo.mdsrc/content/til/2025-10-05-starwind-pagination-breadcrumbs.mdsrc/content/til/2025-10-09-url-sanitization-security.mdsrc/content/til/2025-12-23-accordion-ssr-flash.mdsrc/content/til/2025-12-23-astro-actions-react-forms.mdsrc/content/til/2025-12-30-editorial-typography.mdsrc/content/til/accessibility-ci-cd-pipeline.mdsrc/content/til/accessibility-testing-pipeline-axe.mdsrc/content/til/advanced-focus-management.mdsrc/content/til/astro-content-collections-zod.mdsrc/content/til/cloudinary-image-presets.mdsrc/content/til/content-readability-optimization.mdsrc/content/til/convex-real-time-backend.mdsrc/content/til/notion-cms-astro-loader.mdsrc/content/til/opentelemetry-wide-events.mdsrc/content/til/react-email-templates.mdsrc/content/til/solid-primitives-storage.mdsrc/content/til/stripe-webhook-processing.mdsrc/content/til/suspense-error-boundaries.mdsrc/content/til/tanstack-query-server-state.mdsrc/content/til/timezone-aware-cron-jobs.mdsrc/content/til/view-transitions-api.mdsrc/content/til/wcag-accessibility-audit.md
✅ Files skipped from review due to trivial changes (3)
- src/content/til/cloudinary-image-presets.md
- src/content/til/advanced-focus-management.md
- src/content/til/content-readability-optimization.md
🚧 Files skipped from review as they are similar to previous changes (19)
- src/content/til/accessibility-ci-cd-pipeline.md
- src/content/til/2025-12-30-editorial-typography.md
- src/content/til/2025-07-08-til-content-collection.md
- src/content/til/2025-08-13-notion-astro-loader.md
- src/content/til/2025-06-29-cache-control-strategy.md
- src/content/til/2025-10-05-starwind-pagination-breadcrumbs.md
- src/content/til/view-transitions-api.md
- src/content/til/react-email-templates.md
- src/content/til/2025-12-23-accordion-ssr-flash.md
- src/content/til/2025-08-15-internal-linking-seo.md
- src/content/til/2025-05-27-react-email-welcome.md
- src/content/til/solid-primitives-storage.md
- src/content/til/stripe-webhook-processing.md
- src/content/til/suspense-error-boundaries.md
- src/content/til/2025-12-23-astro-actions-react-forms.md
- src/content/til/2025-06-25-bot-protection.md
- src/content/til/convex-real-time-backend.md
- src/content/til/astro-content-collections-zod.md
- src/content/til/2025-05-18-pagefind-search.md
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Use Astro Content Collections with three sources: `post` (local MDX from `src/data/post/`), `til` (from `src/content/til/`), and `resources` (from Notion database via `notionLoader`)
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Organize components in `src/components/` by feature: `blog/`, `ui/`, `widgets/`, `resources/`, `til/`
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Use Notion integration with vendored `notion-astro-loader` in `vendor/` for live content sync, requiring `NOTION_TOKEN` and `NOTION_RR_RESOURCES_ID` environment variables
Applied to files:
src/content/til/notion-cms-astro-loader.md
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Use Astro Content Collections with three sources: `post` (local MDX from `src/data/post/`), `til` (from `src/content/til/`), and `resources` (from Notion database via `notionLoader`)
Applied to files:
src/content/til/notion-cms-astro-loader.md
📚 Learning: 2025-12-23T18:29:48.423Z
Learnt from: CR
Repo: Resonant-Projects/rproj-website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:29:48.423Z
Learning: Enable server-side rendering via `astrojs/vercel` adapter and experimental `liveContentCollections` for Notion sync
Applied to files:
src/content/til/notion-cms-astro-loader.md
🪛 LanguageTool
src/content/til/2024-12-30-astrowind-foundation.md
[style] ~19-~19: This phrase is redundant (‘O’ stands for ‘optimization’). Use simply “SEO”.
Context: ...th dark mode support out of the box - SEO optimization built into the layout components - **...
(ACRONYM_TAUTOLOGY)
src/content/til/tanstack-query-server-state.md
[grammar] ~11-~11: Use a hyphen to join words.
Context: ...tate I spent weeks building custom data fetching utilities before discovering th...
(QB_NEW_EN_HYPHEN)
[grammar] ~102-~102: Ensure spelling is correct
Context: ...ents(prevWeek), }); } }); ## placeholderData Prevents Loading Flashes typescript...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🔇 Additional comments (12)
src/content/til/tanstack-query-server-state.md (2)
39-48: LGTM!The placeholderData function receives previously watched query data as the first argument, and the example correctly demonstrates usage with
createQueryfrom@tanstack/solid-query. The pattern of centralized query keys and placeholder data for smooth transitions is well-explained.
102-109: LGTM!When using placeholderData, a Query starts in success state with data to display, and the isPlaceholderData flag is set to true on the Query result. The implementation correctly leverages this to prevent loading flashes during state transitions.
src/content/til/notion-cms-astro-loader.md (1)
9-84: Documentation quality is excellent.The content is well-structured, accurate, and provides clear examples of integrating Notion with Astro using the pluggable loaders pattern. Covers the essential aspects: configuration, property mapping, live sync, image handling, and practical caveats. Good alignment with the project's learnings on Notion integration via vendored loaders.
src/content/til/2025-05-27-notion-contact-form.md (1)
1-26: Documentation structure and guidance are solid.The architecture diagram, setup instructions, and benefits section provide clear context for using Notion as a form backend. The practical benefits are well-articulated. Once the code example is corrected, this will be a valuable reference for developers.
Also applies to: 49-56
src/content/til/2025-03-23-vercel-speed-insights.md (1)
1-43: Documentation is well-crafted and accurate.The file effectively contrasts synthetic testing with real user metrics, provides a straightforward Astro integration example, and includes practical metrics to monitor. The real-world insight about regional and mobile performance disparities adds credibility and encourages adoption of RUM practices.
src/content/til/2025-10-09-url-sanitization-security.md (1)
1-65: Excellent security documentation on URL validation best practices.The content clearly explains the vulnerability, provides concrete attack vectors, and demonstrates two progressively better solutions with proper error handling. The emphasis on parsing components over substring matching is exactly right.
src/content/til/accessibility-testing-pipeline-axe.md (2)
1-7: ✅ Frontmatter and metadata are correctly formatted.The YAML frontmatter aligns with Astro Content Collections schema for the
tilcollection. Tags, date, and description are appropriate.
9-43: ✅ Configuration and explanations are clear and accessible.The testing stack overview and configuration example effectively communicate setup requirements. The structure makes it easy to understand the three-tool approach (axe-core, Lighthouse, Puppeteer).
src/content/til/wcag-accessibility-audit.md (4)
1-7: ✅ Frontmatter is correctly configured.The YAML frontmatter is properly formatted with all required fields for the
tilcontent collection.
9-27: ✅ Audit process tools and commands are accurate.The bash commands for
@axe-core/cliand Lighthouse are correct and represent the current best practices for automated accessibility testing. The manual testing checklist is comprehensive and practical.
29-144: ✅ Code examples effectively illustrate accessibility patterns.The before/after examples across color contrast (lines 34-43), focus indicators (lines 49-60), form labels (lines 64-76), skip links (lines 80-99), ARIA labels (lines 103-113), and semantic HTML (lines 133-144) are all syntactically correct and demonstrate real solutions to common accessibility problems. The progression from specific CSS fixes to broader semantic HTML patterns is pedagogically sound.
115-148: ✅ Testing guidance and insights reinforce practical value.The screen reader testing example (lines 119-128) effectively demonstrates the gap between developer assumptions and real accessibility. The closing insight appropriately emphasizes that manual testing with real assistive technologies is irreplaceable and that accessibility improvements often provide secondary benefits (SEO, keyboard navigation for power users).
| ```typescript | ||
| // Customizing the color scheme | ||
| :root { | ||
| --aw-color-primary: /* your brand color */; | ||
| --aw-color-secondary: /* accent color */; | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Correct the code block language label.
The code block contains CSS custom properties syntax (:root selector and CSS variables), not TypeScript. Update the code fence label to reflect the correct language.
✏️ Proposed fix
-// Customizing the color scheme
-:root {
+// Customizing the color scheme (in your CSS file or style tag)
+:root {
--aw-color-primary: /* your brand color */;
--aw-color-secondary: /* accent color */;
}Or simply change the code fence:
-\`\`\`typescript
+\`\`\`css
// Customizing the color scheme
:root {
--aw-color-primary: /* your brand color */;
--aw-color-secondary: /* accent color */;
}
-\`\`\`
+\`\`\`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```typescript | |
| // Customizing the color scheme | |
| :root { | |
| --aw-color-primary: /* your brand color */; | |
| --aw-color-secondary: /* accent color */; | |
| } | |
| ``` |
🤖 Prompt for AI Agents
In @src/content/til/2024-12-30-astrowind-foundation.md around lines 26 - 32, The
code block is labeled as TypeScript but contains CSS (the :root selector and CSS
variables like --aw-color-primary and --aw-color-secondary); update the code
fence label from "typescript" to "css" so the snippet is correctly highlighted
and reflects the actual language.
| ```typescript | ||
| // src/pages/api/submit-to-notion.ts | ||
| import { Client } from '@notionhq/client'; | ||
|
|
||
| const notion = new Client({ auth: process.env.NOTION_TOKEN }); | ||
|
|
||
| export async function POST({ request }) { | ||
| const data = await request.formData(); | ||
|
|
||
| await notion.pages.create({ | ||
| parent: { database_id: process.env.NOTION_DATABASE_ID }, | ||
| properties: { | ||
| Name: { title: [{ text: { content: data.get('name') } }] }, | ||
| Email: { email: data.get('email') }, | ||
| Message: { rich_text: [{ text: { content: data.get('message') } }] }, | ||
| }, | ||
| }); | ||
|
|
||
| return redirect('/thank-you'); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Add missing import and error handling to the code example (unresolved).
The API route example has three unresolved issues that would cause runtime failures:
- Missing import:
redirectis used but not imported from Astro - No input validation: Form fields assumed to exist; missing fields will send
nullto Notion - No error handling: Notion API failures are silently unhandled
These gaps could mislead developers implementing this pattern in production.
🔧 Proposed fix
// src/pages/api/submit-to-notion.ts
import { Client } from '@notionhq/client';
+import { redirect } from 'astro';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
export async function POST({ request }) {
- const data = await request.formData();
+ try {
+ const data = await request.formData();
+ const name = data.get('name')?.toString();
+ const email = data.get('email')?.toString();
+ const message = data.get('message')?.toString();
+
+ if (!name || !email || !message) {
+ return new Response('Missing required fields', { status: 400 });
+ }
- await notion.pages.create({
+ await notion.pages.create({
parent: { database_id: process.env.NOTION_DATABASE_ID },
properties: {
- Name: { title: [{ text: { content: data.get('name') } }] },
- Email: { email: data.get('email') },
- Message: { rich_text: [{ text: { content: data.get('message') } }] },
+ Name: { title: [{ text: { content: name } }] },
+ Email: { email },
+ Message: { rich_text: [{ text: { content: message } }] },
},
- });
+ });
- return redirect('/thank-you');
+ return redirect('/thank-you');
+ } catch (error) {
+ console.error('Form submission error:', error);
+ return new Response('Form submission failed', { status: 500 });
+ }
}| ```typescript | ||
| // src/lib/cloudinary.ts | ||
| import { Cloudinary } from '@cloudinary/url-gen'; | ||
|
|
||
| const cld = new Cloudinary({ | ||
| cloud: { cloudName: process.env.CLOUDINARY_CLOUD_NAME }, | ||
| }); | ||
|
|
||
| export function getOptimizedUrl(publicId: string, width: number) { | ||
| return cld.image(publicId).format('auto').quality('auto').resize(fill().width(width)).toURL(); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add missing import for Cloudinary transformation strategies.
The code example uses fill() without importing it. The @cloudinary/url-gen package installs a transformation-builder-sdk library, and you can use the Transformation Builder reference to find all available transformations. Add the missing import for clarity.
✏️ Proposed fix
// src/lib/cloudinary.ts
import { Cloudinary } from '@cloudinary/url-gen';
+import { fill } from '@cloudinary/url-gen/actions/resize';
const cld = new Cloudinary({
cloud: { cloudName: process.env.CLOUDINARY_CLOUD_NAME },
});
export function getOptimizedUrl(publicId: string, width: number) {
return cld.image(publicId).format('auto').quality('auto').resize(fill().width(width)).toURL();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```typescript | |
| // src/lib/cloudinary.ts | |
| import { Cloudinary } from '@cloudinary/url-gen'; | |
| const cld = new Cloudinary({ | |
| cloud: { cloudName: process.env.CLOUDINARY_CLOUD_NAME }, | |
| }); | |
| export function getOptimizedUrl(publicId: string, width: number) { | |
| return cld.image(publicId).format('auto').quality('auto').resize(fill().width(width)).toURL(); | |
| } | |
| ``` | |
| // src/lib/cloudinary.ts | |
| import { Cloudinary } from '@cloudinary/url-gen'; | |
| import { fill } from '@cloudinary/url-gen/actions/resize'; | |
| const cld = new Cloudinary({ | |
| cloud: { cloudName: process.env.CLOUDINARY_CLOUD_NAME }, | |
| }); | |
| export function getOptimizedUrl(publicId: string, width: number) { | |
| return cld.image(publicId).format('auto').quality('auto').resize(fill().width(width)).toURL(); | |
| } |
🤖 Prompt for AI Agents
In @src/content/til/2025-06-25-cloudinary-integration.md around lines 24 - 35,
The code uses the fill() resize action but doesn't import it, causing a
runtime/compile error; update the Cloudinary module by importing the resize
action (e.g., import { fill } from '@cloudinary/url-gen/actions/resize') and
keep the existing Cloudinary import and getOptimizedUrl implementation so
cld.image(publicId).format('auto').quality('auto').resize(fill().width(width)).toURL()
can call fill() successfully.
| --- | ||
| title: 'Using Notion as a CMS with Astro' | ||
| date: 2025-01-03 | ||
| tags: ['astro', 'notion', 'cms', 'headless'] | ||
| description: 'Today I learned how to use Notion as a headless CMS for Astro using a custom loader with live content sync.' | ||
| draft: false | ||
| --- |
There was a problem hiding this comment.
File naming inconsistency: missing date prefix (unresolved).
This file breaks the naming convention used across other TIL entries. Based on the frontmatter date of 2025-01-03, rename to: 2025-01-03-notion-cms-astro-loader.md to match entries like 2025-08-15-internal-linking-seo.md.
The content itself is well-written and accurate. Please resolve the filename to maintain discoverability and consistency across the TIL collection.
🤖 Prompt for AI Agents
In @src/content/til/notion-cms-astro-loader.md around lines 1 - 7, The file name
doesn't follow the TIL date-prefix convention; rename the file to match its
frontmatter date by using git mv to change
src/content/til/notion-cms-astro-loader.md to
2025-01-03-notion-cms-astro-loader.md (or the same directory path with that
prefix), update any internal references or index entries that point to the old
filename, and commit so history is preserved.
| ```typescript | ||
| // routes/api/telemetry.ts | ||
| export async function POST(req: Request) { | ||
| const events = await req.json(); | ||
|
|
||
| // Forward to Axiom/Datadog/etc | ||
| await fetch('https://api.axiom.co/v1/datasets/events/ingest', { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Bearer ${process.env.AXIOM_TOKEN}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(events), | ||
| }); | ||
|
|
||
| return new Response('OK'); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
This code was previously flagged with specific error handling requirements and remains unchanged. The endpoint lacks:
- Error handling for
req.json()(malformed JSON will throw unhandled exception) - Validation that
process.env.AXIOM_TOKENexists before use (will send undefined Bearer token) - Error handling and response validation for the fetch call to Axiom (upstream failures go undetected)
This creates production reliability issues: malformed client requests crash the endpoint, missing environment configuration fails silently, and upstream service failures are invisible.
🛡️ Suggested error handling improvements
export async function POST(req: Request) {
- const events = await req.json();
+ if (!process.env.AXIOM_TOKEN) {
+ return new Response('Missing AXIOM_TOKEN', { status: 500 });
+ }
+
+ let events;
+ try {
+ events = await req.json();
+ } catch (error) {
+ return new Response('Invalid JSON', { status: 400 });
+ }
- // Forward to Axiom/Datadog/etc
- await fetch('https://api.axiom.co/v1/datasets/events/ingest', {
+ try {
+ const response = await fetch('https://api.axiom.co/v1/datasets/events/ingest', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(events),
+ });
+
+ if (!response.ok) {
+ return new Response('Axiom ingestion failed', { status: 502 });
+ }
+ } catch (error) {
+ return new Response('Telemetry delivery failed', { status: 503 });
+ }
return new Response('OK');
}🤖 Prompt for AI Agents
In @src/content/til/opentelemetry-wide-events.md around lines 125 - 142, The
POST handler currently calls req.json(), uses process.env.AXIOM_TOKEN, and
forwards with fetch without any error handling or validation; update the POST
function to wrap req.json() in a try/catch to return a 400 on malformed JSON,
check that process.env.AXIOM_TOKEN is present and return a 500/422 with a clear
message if missing, and wrap the fetch call in try/catch then validate the
response status (treat non-2xx as an error), returning an appropriate error
response and logging details; keep these changes scoped to the POST function and
reference req.json(), process.env.AXIOM_TOKEN, and the fetch to
'https://api.axiom.co/v1/datasets/events/ingest'.
| ```typescript | ||
| // convex/crons.ts | ||
| export const cronJobs = cronJobs({ | ||
| // Run every hour, process users whose local time is now 8 AM | ||
| dailyEmailDispatch: { | ||
| schedule: '0 * * * *', // Every hour on the hour | ||
| handler: dailyEmailHandler, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
This code was previously flagged as using incorrect Convex API syntax and remains unchanged. The current implementation uses object-literal notation which is not valid Convex API:
// ❌ INVALID: object-literal syntax
export const cronJobs = cronJobs({
dailyEmailDispatch: {
schedule: '0 * * * *',
handler: dailyEmailHandler,
},
});According to Convex documentation, cron jobs must be registered using the crons instance methods (cron(), interval(), hourly(), etc.), and handlers must be FunctionReferences, not raw function objects. This creates critical documentation risk: readers following this pattern will write non-functional code.
🔧 Correct Convex cron API usage
-// convex/crons.ts
-export const cronJobs = cronJobs({
- // Run every hour, process users whose local time is now 8 AM
- dailyEmailDispatch: {
- schedule: '0 * * * *', // Every hour on the hour
- handler: dailyEmailHandler,
- },
-});
+// convex/crons.ts
+import { cronJobs } from 'convex/server';
+import { internal } from './_generated/api';
+
+const crons = cronJobs();
+
+// Run every hour on the hour, process users whose local time is now 8 AM
+crons.interval('dailyEmailDispatch', { minutes: 60 }, internal.emails.dailyEmailHandler);
+
+export default crons;Or using the hourly convenience method:
crons.hourly('dailyEmailDispatch', internal.emails.dailyEmailHandler);Key requirements:
- Use
cronJobs()instance methods, not object literal syntax - Pass a FunctionReference (e.g.,
internal.emails.dailyEmailHandler), not a raw function - Export the crons instance as
export default crons;
🤖 Prompt for AI Agents
In @src/content/til/timezone-aware-cron-jobs.md around lines 29 - 37, The
current export uses invalid object-literal Convex syntax (cronJobs =
cronJobs({...})) and passes a raw function; replace this with a crons instance
registration using the instance methods and a FunctionReference: create a crons
instance and register the job with crons.cron(...) or
crons.hourly('dailyEmailDispatch', internal.emails.dailyEmailHandler) (or
crons.cron('dailyEmailDispatch', '0 * * * *',
internal.emails.dailyEmailHandler)), ensure the handler is a FunctionReference
like internal.emails.dailyEmailHandler rather than dailyEmailHandler, and export
the instance as export default crons.
Adding a bunch of TIL entries based on my development history
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.