Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/content/til/2024-12-30-astrowind-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
title: 'Starting with AstroWind: The Foundation of Resonant Projects'
date: 2024-12-30
tags: ['astro', 'astrowind', 'tailwind', 'web-development']
description: 'Today I learned how starting with a well-structured template like AstroWind can accelerate custom website development while still allowing complete customization.'
draft: false
---

# Starting with AstroWind: The Foundation of Resonant Projects

Building a custom website from scratch is rewarding, but starting with a solid foundation can save weeks of work. Today I began customizing the AstroWind template for Resonant Projects.

## Why Start with a Template?

AstroWind provides:

- **Pre-built Astro components** with proper TypeScript types
- **Tailwind CSS integration** with dark mode support out of the box
- **SEO optimization** built into the layout components
- **Performance optimizations** like image optimization and lazy loading

## Initial Customization Steps

The first changes I made were to themes and colors, establishing the visual identity:

```typescript
// Customizing the color scheme
:root {
--aw-color-primary: /* your brand color */;
--aw-color-secondary: /* accent color */;
}
```
Comment on lines +26 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
```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.


## Key Takeaway

Don't reinvent the wheel—but also don't be afraid to modify every aspect of a template. The goal is to use it as scaffolding, not a constraint. Within a few commits, the site was already distinctly "Resonant Projects" rather than generic AstroWind.
43 changes: 43 additions & 0 deletions src/content/til/2025-03-23-vercel-speed-insights.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: 'Integrating Vercel Speed Insights for Real User Metrics'
date: 2025-03-23
tags: ['vercel', 'performance', 'monitoring', 'web-vitals']
description: 'Today I learned how to integrate Vercel Speed Insights to get real-world performance metrics from actual users, not just Lighthouse synthetic tests.'
draft: false
---

# Integrating Vercel Speed Insights for Real User Metrics

Lighthouse scores are great, but they're synthetic. Today I integrated Vercel Speed Insights to get real user metrics (RUM).

## Why Real User Metrics Matter

Lighthouse runs in a controlled environment. Real users have:

- Varying network conditions
- Different device capabilities
- Various geographic locations
- Actual interaction patterns

## Quick Integration

```astro
---
import { SpeedInsights } from '@vercel/speed-insights/astro';
---

<SpeedInsights />
```

That's it! Vercel automatically starts collecting:

- **LCP** (Largest Contentful Paint)
- **FID** (First Input Delay)
- **CLS** (Cumulative Layout Shift)
- **TTFB** (Time to First Byte)

## Insights from Real Data

After a week of data, I discovered that mobile users in certain regions were experiencing significantly slower TTFB than my Lighthouse tests suggested. This led to investigating edge caching strategies.

The lesson: synthetic tests are a starting point, but real user metrics tell the true story.
53 changes: 53 additions & 0 deletions src/content/til/2025-05-18-pagefind-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: 'Adding Lightning-Fast Search with Pagefind'
date: 2025-05-18
tags: ['astro', 'search', 'pagefind', 'static-site']
description: 'Today I learned how Pagefind provides instant client-side search for static sites without any backend infrastructure.'
draft: false
---

# Adding Lightning-Fast Search with Pagefind

Static sites traditionally struggle with search—you either need a backend service or rely on external providers. Today I integrated Pagefind, and it's a game-changer.

## What Makes Pagefind Special

Pagefind builds a search index at build time and includes a tiny client-side search that:

- Works entirely client-side (no server needed)
- Has a compressed index averaging ~100kB for most sites
- Provides instant results with typo tolerance
- Supports multiple languages

## Integration in Astro

```bash
pnpm add pagefind
```

Then in your build script:

```json
{
"scripts": {
"postbuild": "pagefind --site dist"
}
}
```

And in your search component:

```astro
<div id="search"></div>
<script>
import * as pagefind from '/pagefind/pagefind.js';
pagefind.init();
new PagefindUI({ element: '#search' });
</script>
```

## Key Insight

The search index is built at deploy time, so it's always in sync with your content. No more worrying about stale search results or webhook failures. When your site builds, your search is automatically updated.

Perfect for content-heavy static sites where you want great search UX without operational complexity.
56 changes: 56 additions & 0 deletions src/content/til/2025-05-27-notion-contact-form.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: 'Building a Notion-Powered Contact Form'
date: 2025-05-27
tags: ['notion', 'api', 'forms', 'astro']
description: 'Today I learned how to submit form data directly to a Notion database, creating a serverless CRM-like system.'
draft: false
---

# Building a Notion-Powered Contact Form

Why pay for a form backend when Notion can be your database? Today I implemented a contact form that submits directly to Notion.

## The Architecture

```
User submits form → Astro API route → Notion API → Database entry
```

## Setting Up the Notion Integration

1. Create a Notion integration at notion.so/my-integrations
2. Share your database with the integration
3. Store the token securely in environment variables

## The API Route

```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');
}
```
Comment on lines +27 to +47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add missing import and error handling to the code example (unresolved).

The API route example has three unresolved issues that would cause runtime failures:

  1. Missing import: redirect is used but not imported from Astro
  2. No input validation: Form fields assumed to exist; missing fields will send null to Notion
  3. 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 });
+  }
 }


## Benefits Over Traditional Forms

- **No additional service costs** - Notion's API is free
- **Built-in collaboration** - Team can see and respond to inquiries
- **Flexible schema** - Add fields to your database anytime
- **Views and filters** - Sort by date, filter by status, etc.

The key insight: Notion databases are surprisingly powerful backends for simple data collection needs.
52 changes: 52 additions & 0 deletions src/content/til/2025-05-27-react-email-welcome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: 'Sending Beautiful Welcome Emails with React Email'
date: 2025-05-27
tags: ['react-email', 'email', 'automation', 'typescript']
description: 'Today I learned how React Email makes building and previewing HTML emails as easy as building React components.'
draft: false
---

# Sending Beautiful Welcome Emails with React Email

HTML emails are notoriously painful to build. Tables, inline styles, client quirks... Today I discovered React Email, and it changes everything.

## Why React Email?

- Write emails using React components
- Live preview during development
- Built-in components for common patterns
- TypeScript support out of the box

## Creating a Welcome Email

```tsx
// emails/WelcomeEmail.tsx
import { Html, Head, Body, Container, Text, Button } from '@react-email/components';

export const WelcomeEmail = ({ name }: { name: string }) => (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif' }}>
<Container>
<Text>Welcome, {name}!</Text>
<Text>Thanks for reaching out to Resonant Projects.</Text>
<Button href="https://resonantprojects.art">Visit Our Site</Button>
</Container>
</Body>
</Html>
);
```

## Rendering and Sending

```typescript
import { render } from '@react-email/render';
import { WelcomeEmail } from '../emails/WelcomeEmail';

const html = render(<WelcomeEmail name="Keith" />);
// Send via your email provider (Resend, SendGrid, etc.)
```

## Key Insight

The preview server (`pnpm email dev`) lets you iterate on email designs instantly. No more "send test email → check inbox → tweak → repeat" cycles. Build your emails like you build your UI.
63 changes: 63 additions & 0 deletions src/content/til/2025-06-25-bot-protection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: 'Protecting Forms with Vercel Bot Protection'
date: 2025-06-25
tags: ['security', 'vercel', 'forms', 'spam-prevention']
description: 'Today I learned how to integrate Vercel bot protection to stop spam submissions without annoying CAPTCHAs.'
draft: false
---

# Protecting Forms with Vercel Bot Protection

Spam bots love contact forms. Traditional CAPTCHAs frustrate real users. Today I implemented Vercel's invisible bot protection.

## The Problem with CAPTCHAs

- Accessibility issues
- User friction
- Increasingly difficult to solve (even for humans!)
- Bots are getting better at solving them anyway

## Vercel's Approach

Vercel's bot protection works invisibly by analyzing:

- Request patterns
- Browser fingerprints
- Behavioral signals

No user interaction required.

## Implementation

Add to your `vercel.json`:

```json
{
"firewall": {
"rules": [
{
"action": "challenge",
"source": "api/submit-to-notion"
}
]
}
}
```

On the server side, verify the bot check:

```typescript
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
}
Comment on lines +50 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.

```

## Results

After enabling bot protection, spam submissions dropped to nearly zero while legitimate submissions continued unaffected. The invisible nature means better UX with better security.
50 changes: 50 additions & 0 deletions src/content/til/2025-06-25-cloudinary-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: 'Dynamic Image Optimization with Cloudinary'
date: 2025-06-25
tags: ['cloudinary', 'images', 'optimization', 'cdn']
description: 'Today I learned how Cloudinary can transform and optimize images on-the-fly, reducing the need for pre-processing.'
draft: false
---

# Dynamic Image Optimization with Cloudinary

Managing images for a website means dealing with multiple sizes, formats, and quality levels. Today I integrated Cloudinary for dynamic image transformation.

## Why Not Just Use Astro's Image Optimization?

Astro's built-in image optimization is great for static images, but Cloudinary excels when you need:

- Dynamic transformations based on context
- Images from external sources (like Notion)
- Real-time cropping, resizing, and effects
- Automatic format selection (WebP, AVIF)

## Implementation

```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();
}
```
Comment on lines +24 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
```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.


## In Components

```astro
---
import { getOptimizedUrl } from '@/lib/cloudinary';
const heroUrl = getOptimizedUrl('hero-image', 1200);
---

<img src={heroUrl} alt="Hero" loading="lazy" />
```

## Key Insight

The URL-based transformation API means you can request exactly what you need. A 400px thumbnail? Change the URL parameter. Need a blurred placeholder? Add `e_blur:1000`. No build-time processing, just instant transformations at the CDN edge.
Loading