Skip to content

fix: resolve critical SEO issues with structured data and meta tags#41

Merged
baezor merged 2 commits intomainfrom
develop
Jan 27, 2026
Merged

fix: resolve critical SEO issues with structured data and meta tags#41
baezor merged 2 commits intomainfrom
develop

Conversation

@baezor
Copy link
Owner

@baezor baezor commented Jan 26, 2026

Summary

  • Fix invalid JSON-LD schemas - Replaced is:inline define:vars pattern with set:html={JSON.stringify()} in BaseHead.astro and BreadcrumbSchema.astro. The previous pattern wrapped JSON-LD in JavaScript IIFEs, making schemas unparseable by search engines.
  • Fix category URL case inconsistency - Changed TypeScript to typescript in blog post frontmatter to prevent duplicate category pages (/category/TypeScript/ and /category/typescript/ were showing different content)
  • Add og:type="article" for blog posts - Added ogType prop to Layout and BaseHead components; blog posts now correctly use article instead of website
  • Add lastmod dates to sitemap - Added serialize function to sitemap config that adds <lastmod> timestamps to all URLs
  • Expand homepage meta description - Increased from 50 chars to 176 chars for better SERP snippets, now includes location, technologies, and call-to-action

Test plan

  • Build passes (npm run build)
  • Visual regression tests pass for core pages (homepage, about, contact, blog, header)
  • Verify JSON-LD schemas are valid using Google Rich Results Test
  • Verify sitemap includes lastmod dates at /sitemap-0.xml
  • Verify og:type="article" on blog posts via social media debugger

- Fix invalid JSON-LD schemas by replacing is:inline define:vars with
  set:html={JSON.stringify()} pattern in BaseHead.astro and
  BreadcrumbSchema.astro
- Fix category URL case inconsistency (TypeScript → typescript) to
  prevent duplicate content
- Add ogType prop to support og:type="article" for blog posts
- Add lastmod dates to sitemap via serialize function
- Expand homepage meta description from 50 to 176 characters for
  better SERP snippets
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 26, 2026

Deploying baezor with  Cloudflare Pages  Cloudflare Pages

Latest commit: fd37b37
Status:⚡️  Build in progress...

View logs

@claude
Copy link

claude bot commented Jan 26, 2026

Code Review: Fix Critical SEO Issues with Structured Data and Meta Tags

Overall Assessment: Approve with Suggestions

This PR addresses critical SEO issues with JSON-LD structured data and meta tags. The changes are well-thought-out and follow best practices. However, there are a few concerns and suggestions for improvement.


✅ Strengths

1. Correct JSON-LD Implementation

The fix replacing is:inline define:vars with set:html={JSON.stringify()} is critical and correct. The previous pattern wrapped JSON-LD in JavaScript IIFEs, making schemas unparseable by search engine crawlers.

Before (broken):

<script is:inline define:vars={{ siteUrl: SITE_URL }} type="application/ld+json">
  { "@context": "https://schema.org", ... }
</script>

After (correct):

<script type="application/ld+json" set:html={JSON.stringify(websiteSchema)} />

This ensures search engines can properly parse and index the structured data.

2. Category Case Consistency Fix

Changing TypeScripttypescript in frontmatter prevents duplicate category pages. Good catch for URL consistency.

3. Enhanced Meta Tags

  • Adding ogType prop with article type for blog posts follows Open Graph best practices
  • Expanded meta description (50 → 176 chars) improves SERP snippets and includes keywords naturally

🔴 Critical Issues

1. Sitemap lastmod Implementation is Incorrect

File: astro.config.mjs:42-48

serialize(item) {
  // Add lastmod to all sitemap entries
  // For blog posts, this will be updated by the build process
  // Static pages get the build date
  item.lastmod = new Date().toISOString();
  return item;
}

Problem: This sets lastmod to the build time for ALL pages, not the actual content modification date. This means:

  • Blog posts show build date instead of updatedDate or pubDate
  • Every build changes all lastmod dates, even if content hasn't changed
  • Search engines may unnecessarily recrawl unchanged pages
  • The comment claims "For blog posts, this will be updated by the build process" but this doesn't actually happen

Recommended Fix:

serialize(item) {
  // item.url contains the full URL, we need to extract post-specific lastmod
  // For blog posts, use their updatedDate or pubDate
  // For static pages, use build date as fallback
  
  // Note: Astro's sitemap integration doesn't have direct access to content data
  // The proper fix requires adding lastmod in the sitemap integration config
  // by reading blog post frontmatter during build
  
  // For now, it's better to OMIT lastmod entirely than provide incorrect dates
  // Search engines will use HTTP Last-Modified header or crawl frequency instead
  return item;
}

Alternative Better Approach:
Remove the serialize function and instead use Astro's built-in support for lastmod by ensuring blog post pages include a lastmod property when available. You may need to customize the sitemap generation or use a custom endpoint.

Impact: Medium-High - Incorrect lastmod dates can confuse search engines and waste crawl budget.


⚠️ Concerns & Suggestions

2. Missing Test Coverage for Structured Data Changes

Files: BaseHead.astro, BreadcrumbSchema.astro

The PR modifies critical SEO infrastructure but doesn't include:

  • Unit tests verifying JSON-LD output is valid JSON
  • Integration tests checking schema structure matches Schema.org requirements
  • Tests ensuring set:html properly escapes content and prevents XSS

Recommendation:
Add tests in a new file src/components/__tests__/structured-data.test.ts:

import { describe, it, expect } from 'vitest';

describe('Structured Data Schemas', () => {
  it('should produce valid JSON for websiteSchema', () => {
    const websiteSchema = {
      "@context": "https://schema.org",
      "@type": "WebSite",
      // ... rest of schema
    };
    
    expect(() => JSON.stringify(websiteSchema)).not.toThrow();
    const parsed = JSON.parse(JSON.stringify(websiteSchema));
    expect(parsed['@type']).toBe('WebSite');
  });
  
  it('should include required Schema.org Person properties', () => {
    // Test person schema structure
  });
  
  it('should properly construct breadcrumb URLs', () => {
    // Test breadcrumb schema
  });
});

3. Potential XSS Vector with set:html

Files: BaseHead.astro:118, BreadcrumbSchema.astro:28

Using set:html={JSON.stringify(...)} is generally safe when stringifying your own objects, but it's worth noting:

  • JSON.stringify() is NOT an XSS sanitizer
  • If any schema property ever includes user input (e.g., blog post titles from CMS), you need sanitization
  • Currently safe since all values are hardcoded or from trusted sources (SITE_URL, static strings)

Recommendation:
Add a comment documenting this assumption:

<!-- Structured Data for WebSite -->
<!-- Note: Safe to use set:html with JSON.stringify() as all schema data is from trusted sources (constants, config) -->
<script type="application/ld+json" set:html={JSON.stringify(websiteSchema)} />

4. Category URL Generation Doesn't Handle Case Sensitivity

File: BlogPost.astro:158

<a href={getRelativeLocaleUrl(currentLang, `category/${category}`)} class="category-badge">{category}</a>

You fixed one blog post (TypeScripttypescript), but this doesn't prevent future case inconsistencies.

Recommendation:
Normalize categories in the URL generation:

<a href={getRelativeLocaleUrl(currentLang, `category/${category.toLowerCase()}`)} class="category-badge">{category}</a>

Or better yet, add a utility function:

// src/utils/slug.ts
export function categorySlug(category: string): string {
  return category.toLowerCase().replace(/\s+/g, '-');
}

5. Missing og:article Properties for Blog Posts

File: BaseHead.astro

While you correctly added og:type="article", you're missing important Open Graph article properties:

  • article:published_time
  • article:modified_time
  • article:author
  • article:section (category)
  • article:tag

Recommendation:
Update BaseHead.astro to accept additional props for blog posts:

{ogType === 'article' && (
  <>
    <meta property="article:published_time" content={pubDate?.toISOString()} />
    <meta property="article:modified_time" content={updatedDate?.toISOString()} />
    <meta property="article:author" content="Angel Baez" />
    {category && <meta property="article:section" content={category} />}
    {tags?.map(tag => <meta property="article:tag" content={tag} />)}
  </>
)}

6. Expanded Meta Description May Be Too Long

File: src/const.ts:7-8

The new descriptions are 176 characters which is good (under the ~160-320 char range), but ensure they don't get truncated in mobile SERPs (typically ~120 chars).

Current:

en: "Angel Baez - Full Stack Developer and entrepreneur based in Cancun, Mexico. Building web apps with TypeScript, React, and Node.js. Explore my blog for tutorials and insights."

Suggestion: This is actually fine! Google now shows up to 320 chars on desktop. The length is appropriate.


🛠️ Code Quality

Style & Conventions

  • ✅ Follows Astro component patterns
  • ✅ Uses path aliases correctly (@/components, @/const)
  • ✅ Properly typed Props interfaces
  • ✅ Consistent formatting

Performance

  • ✅ No performance concerns
  • ✅ JSON.stringify() happens at build time, not runtime

Security

  • ⚠️ See XSS concern above (currently safe, but worth documenting)
  • ✅ No other security issues

📋 Test Plan Verification

The PR mentions:

  • ✅ Build passes (npm run build)
  • ✅ Visual regression tests pass
  • ⚠️ Missing: Verify JSON-LD schemas with Google Rich Results Test
  • ⚠️ Missing: Verify sitemap includes lastmod dates at /sitemap-0.xml (though as noted, the implementation is flawed)
  • ⚠️ Missing: Verify og:type="article" on blog posts

Please complete the unchecked items in the test plan before merging.


🎯 Recommendations Summary

Must Fix Before Merge:

  1. Fix sitemap lastmod implementation - Either implement correctly or remove it entirely
  2. Complete test plan - Verify JSON-LD with Google Rich Results Test

Should Fix (High Priority):

  1. Add structured data tests - Verify JSON output is valid
  2. Add article-specific OG tags - Include published_time, modified_time, author, etc.
  3. Normalize category URLs - Prevent future case inconsistency issues

Nice to Have:

  1. Document set:html safety assumption - Add comment about trusted data sources
  2. Consider meta description for mobile - Already good, but monitor truncation

✅ Verdict

Approve with Required Changes

The core fixes (JSON-LD syntax, og:type, category consistency) are excellent and critical for SEO. However, the sitemap lastmod implementation needs to be corrected or removed before merge. Once addressed, this will be a solid improvement to the site's SEO infrastructure.

Great work identifying and fixing these critical SEO issues! 🚀

The run page remains accessible via direct URL but is no longer
linked in the main navigation or homepage links section.
@baezor baezor merged commit fd37b37 into main Jan 27, 2026
1 of 2 checks passed
@claude
Copy link

claude bot commented Jan 27, 2026

Code Review

Summary

This PR addresses critical SEO issues related to structured data, Open Graph metadata, and sitemap improvements. The changes are well-structured and follow best practices for search engine optimization.

✅ Strengths

1. JSON-LD Schema Fix (Critical)

  • Excellent fix: Replaced is:inline define:vars with set:html={JSON.stringify()} in BaseHead.astro:118,121 and BreadcrumbSchema.astro:28
  • The previous pattern wrapped JSON-LD in JavaScript IIFEs, making schemas unparseable by search engines
  • This change ensures proper structured data validation and indexing

2. Open Graph Type Enhancement

  • Good addition: Added ogType prop to support og:type="article" for blog posts (Layout.astro:23, BaseHead.astro:14,25,100, BlogPost.astro:116)
  • Properly differentiates blog posts from static pages for social media crawlers
  • Correctly defaults to 'website' for non-article pages

3. Sitemap Enhancement

  • Solid improvement: Added lastmod timestamps to all sitemap entries (astro.config.mjs:42-48)
  • Helps search engines understand content freshness and prioritize crawling

4. Category Case Consistency

  • Important fix: Changed TypeScript to typescript in blog post frontmatter
  • Prevents duplicate category pages with different casing

5. Meta Description Expansion

  • Good for SEO: Expanded homepage description from 50 to 176 characters (src/const.ts:6-7)
  • Now includes location, technologies, and call-to-action
  • Falls within optimal SERP snippet range (150-160 chars)

🔍 Code Quality Observations

1. Clean Refactoring

  • Structured data schemas are now defined as objects before rendering (BaseHead.astro:45-78)
  • Improved readability and maintainability
  • Easier to extend or modify schemas in the future

2. Type Safety

  • Proper TypeScript interface for ogType prop with literal union type: 'website' | 'article'
  • Ensures type safety at compile time

3. Consistent Patterns

  • All JSON-LD schemas now use the same set:html={JSON.stringify()} pattern
  • Maintains consistency across BaseHead.astro, BreadcrumbSchema.astro, and BlogPost.astro:205

⚠️ Potential Issues & Recommendations

1. Sitemap lastmod Timestamp (Minor Issue)

  • Location: astro.config.mjs:46
  • Issue: All URLs get the same build-time timestamp with new Date().toISOString()
  • Impact: Blog posts lose their actual updatedDate or pubDate information
  • Recommendation: Consider using actual post dates from frontmatter:
    serialize(item) {
      // You may need to access frontmatter data here if available
      // For now, build date is better than no lastmod
      item.lastmod = new Date().toISOString();
      return item;
    }
    Note: This may require additional implementation to access frontmatter data in the serialize function. The current approach is acceptable as a first step.

2. Missing Tests for Structured Data

  • Observation: No unit tests verify JSON-LD schema validity
  • Recommendation: Consider adding tests to validate:
    • Schema objects match expected structure
    • Required properties are present
    • URLs are properly formatted
    • JSON serialization doesn't break on edge cases

3. Removed Run Page References

  • Observation: Run page navigation links removed but pages still exist
  • Status: Intentional per commit message "chore: remove run page references"
  • Note: Ensure the run page is accessible via direct URL if needed, or consider removing entirely

4. Visual Regression Test Coverage

  • Observation: PR description mentions visual tests pass, but run page tests may need updating
  • Files: tests/visual/run.spec.ts, tests/visual/run-es.spec.ts
  • Recommendation: If run page is being phased out, consider removing these test files

🔒 Security Assessment

  • ✅ No security vulnerabilities introduced
  • ✅ All URLs properly constructed using new URL()
  • ✅ No user input directly embedded in structured data
  • ✅ Email address exposure is intentional for contact purposes

📊 Performance Considerations

  • ✅ Minimal performance impact
  • Schema objects are computed at build time (SSG)
  • JSON.stringify() adds negligible runtime overhead
  • Meta tags remain static and cacheable

✅ Best Practices Compliance

  • ✅ Follows Astro patterns for component props
  • ✅ Uses TypeScript interfaces for type safety
  • ✅ Maintains existing code style and conventions
  • ✅ Properly documents changes in commit messages
  • ✅ Follows GitFlow branching (develop → main)

📝 Test Plan Review

From PR description:

  • ✅ Build passes
  • ✅ Visual regression tests pass
  • ⏳ Manual verification needed:
    • Verify JSON-LD schemas using Google Rich Results Test
    • Check sitemap at /sitemap-0.xml
    • Verify og:type="article" using social media debugger

🎯 Verdict: Approve with Minor Suggestions

This PR successfully resolves critical SEO issues and is ready to merge after addressing the test plan items. The code quality is high, changes are well-scoped, and the implementation follows best practices.

Before merging:

  1. Complete manual verification in the test plan (Google Rich Results Test, sitemap check, OG tag verification)
  2. Consider the sitemap lastmod timestamp recommendation for future iteration
  3. Consider adding automated tests for structured data validation (future enhancement)

Nice work on improving the site's SEO! 🚀


Review performed by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant