Skip to content

Add comprehensive social sharing metadata#17

Merged
ooloth merged 35 commits intomainfrom
improve-metadata
Dec 21, 2025
Merged

Add comprehensive social sharing metadata#17
ooloth merged 35 commits intomainfrom
improve-metadata

Conversation

@ooloth
Copy link
Owner

@ooloth ooloth commented Dec 21, 2025

✅ What

  • Adds complete OpenGraph metadata to all pages (og:title, og:image, og:url, og:type, og:site_name, og:locale, og:description)
  • Adds Twitter Card metadata (twitter:card, twitter:creator, twitter:title, twitter:image, twitter:description)
  • Adds JSON-LD structured data to article pages (schema.org/Article with author, dates, images)
  • Transforms blog post featured images to 1200x630 for social sharing (Cloudinary)
  • Generates a default OG image for pages without featured images
  • Validates all metadata in CI by parsing static HTML and checking tag content
  • Consolidates metadata constants into a single source of truth

🤔 Why

  • Social platforms (Twitter, LinkedIn, Facebook, Slack, Discord, iMessage) now show proper preview cards instead of generic unfurls
  • Search engines get structured data for better article understanding

👩‍🔬 How to validate

  1. npm run build - Expect build to succeed with metadata validation passing
  2. Open out/index.html - Expect to see og:title, og:image, og:url, twitter:card meta tags
  3. Open any out/[post-slug]/index.html - Expect to see article-specific metadata including article:published_time and JSON-LD script
  4. Paste https://michaeluloth.com/ into Twitter Card Validator - Expect to see preview card with your photo
  5. Paste a blog post URL into the same validator - Expect to see card with post title and featured image (1200x630)
  6. Check the OG image dimensions: inspect network tab, find the Cloudinary URL - Expect w_1200,h_630 in the transformation params

Create transformCloudinaryForOG() to standardize OpenGraph image dimensions.
Inserts c_fill,w_1200,h_630 transformation parameters into Cloudinary URLs
to ensure images meet OG requirements (1200x630px).

Non-Cloudinary URLs are returned unchanged for compatibility.
Use transformCloudinaryForOG() for both metadata and JSON-LD images
in blog post pages. Ensures featured images are properly sized for
OpenGraph sharing (1200x630px) while preserving original URLs for
non-Cloudinary images.
Add OpenGraph URL metadata to root layout, blog index, and likes page.
The og:url tag helps social platforms accurately attribute and
deduplicate shared content.
Update validateOgImage() to read local images from the out/ directory
when validating static builds. Detects michaeluloth.com URLs and reads
them from the filesystem instead of fetching over HTTP.

This enables metadata validation to work with static export where
images are part of the build output rather than served dynamically.
Expand metadata validation to check required Twitter Card meta tags
(card, creator, title, description, image) alongside existing OpenGraph
validation. This ensures social sharing works correctly on both Facebook
(OpenGraph) and Twitter/X (Twitter Cards).
Add siteName and locale to OpenGraph metadata, and creator to Twitter
metadata for individual article pages. These fields ensure proper social
card rendering across platforms and satisfy the expanded validation checks.
Complete OpenGraph metadata for blog listing page by adding siteName
and locale fields, matching the pattern established for article pages.
Complete OpenGraph metadata for likes page by adding siteName and locale
fields, ensuring consistent social sharing across all site pages.
- Update home page tests to remove skipCache parameter references
- Update layout test to expect url field in OpenGraph config
- All tests now pass (444/444)
- Fix type errors by removing searchParams from test function calls
- Run prettier to fix formatting issues
- All checks now pass: types, formatting, tests (444/444)
@ooloth ooloth marked this pull request as ready for review December 21, 2025 05:15
Copilot AI review requested due to automatic review settings December 21, 2025 05:15
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds comprehensive social sharing metadata and structured data support across the site to improve how links appear when shared on social platforms (Twitter, LinkedIn, Facebook, Slack, Discord, iMessage) and in search engines.

Key changes:

  • Consolidated metadata constants into utils/metadata.ts for single source of truth
  • Added complete OpenGraph and Twitter Card metadata to all pages
  • Added JSON-LD structured data to article pages for better search engine understanding
  • Created automated metadata validation that parses static HTML and runs in CI

Reviewed changes

Copilot reviewed 27 out of 32 changed files in this pull request and generated no comments.

Show a summary per file
File Description
utils/metadata.ts New centralized metadata constants (SITE_URL, SITE_NAME, SITE_DESCRIPTION, SITE_AUTHOR, DEFAULT_OG_IMAGE)
utils/metadata.test.ts Tests verifying metadata constant values
utils/constants.ts Removed (SITE_URL moved to metadata.ts)
package.json Added metadata generation/validation scripts and dependencies (cheerio, satori, sharp, tsx)
next.config.ts Added static export output mode
metadata/validate-metadata.ts New HTML-based metadata validation script for CI
metadata/generate-og-image.ts Script to generate default 1200x630 OG image using satori/sharp
io/cloudinary/ogImageTransforms.ts Utility to transform Cloudinary URLs to OG dimensions (1200x630)
io/cloudinary/ogImageTransforms.test.ts Tests for Cloudinary OG transformation logic
app/sitemap.ts Updated import path for SITE_URL
app/rss.xml/route.ts Updated import path for SITE_URL
app/robots.ts Updated import path for SITE_URL
app/likes/page.tsx Added OpenGraph and Twitter metadata; removed skipCache functionality
app/likes/page.test.tsx Added metadata tests; removed skipCache test cases
app/layout.tsx Enhanced with metadataBase, title template, authors, OpenGraph, and Twitter configs
app/layout.test.tsx Added tests for new metadata fields
app/(prose)/page.tsx Removed skipCache functionality
app/(prose)/page.test.tsx Removed skipCache test cases
app/(prose)/blog/page.tsx Added OpenGraph and Twitter metadata; removed skipCache functionality
app/(prose)/blog/page.test.tsx Added metadata tests; removed skipCache test cases
app/(prose)/[slug]/page.tsx Added generateMetadata function with article-specific OG data and JSON-LD structured data; removed skipCache functionality
app/(prose)/[slug]/page.test.tsx Added comprehensive tests for generateMetadata and JSON-LD; removed skipCache test cases
.github/workflows/test.yml Added metadata validation job to CI pipeline
.beads/issues.jsonl Updated issue tracking records
AGENTS.md, @AGENTS.md, .beads/README.md Minor formatting adjustments

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@claude
Copy link

claude bot commented Dec 21, 2025

Pull Request Review: Add comprehensive social sharing metadata

This is an excellent, well-structured PR that adds comprehensive metadata support. The implementation is thorough, well-tested, and follows the repository's conventions. Here's my detailed review:


✅ Strengths

1. Excellent Test Coverage

  • Comprehensive tests for all metadata functions and validation
  • Build-time metadata validation using static HTML analysis (very clever!)
  • JSON-LD structured data validation
  • CI integration ensures metadata correctness on every build

2. Well-Organized Code

  • Clear separation of concerns (constants, transforms, validation, generation)
  • Consistent use of shared constants across the codebase
  • Good documentation and comments explaining the "why"

3. Robust Implementation

  • Proper handling of optional fields (description, featured images)
  • Cloudinary transformation utility ensures correct OG image dimensions
  • Validation covers both OpenGraph and Twitter Card metadata
  • Local and remote image handling in validation script

4. Developer Experience

  • Clear commit messages with descriptive bodies
  • Helpful npm scripts (generate:og-image, test:metadata)
  • Good PR description with validation steps

🔍 Issues & Recommendations

High Priority

1. Security: XSS Risk in JSON-LD (app/(prose)/[slug]/page.tsx:98)

<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />

Issue: While JSON.stringify() generally escapes HTML entities, there's a potential XSS vulnerability if post data contains </script> sequences.

Recommendation: Add explicit escaping:

<script 
  type="application/ld+json" 
  dangerouslySetInnerHTML={{ 
    __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') 
  }} 
/>

Or use a dedicated library like serialize-javascript for safer serialization.

2. Error Handling: Missing Input Validation (metadata/validate-metadata.ts)

The validation script doesn't validate inputs or handle edge cases:

// Line 239: No validation that imageUrl is a string
const imagePath = imageUrl.replace('https://michaeluloth.com/', '')

Recommendation: Add type guards and validation:

if (typeof imageUrl !== 'string' || !imageUrl.startsWith('https://michaeluloth.com/')) {
  errors.push({ page: pageName, error: `Invalid local image URL: ${imageUrl}` })
  return
}

Medium Priority

3. Code Quality: Duplicate URL Construction (app/(prose)/[slug]/page.tsx)

The URL is constructed twice (lines 25 and 53):

const url = `${SITE_URL}${slug}/`  // Line 25
const url = `${SITE_URL}${slug}/`  // Line 53

Recommendation: Extract to a helper function:

function getPostUrl(slug: string): string {
  return `${SITE_URL}${slug}/`
}

4. Code Quality: Magic Numbers (io/cloudinary/ogImageTransforms.ts:19)

Hardcoded dimensions without explanation:

return url.replace('/upload/', '/upload/c_fill,w_1200,h_630/')

Recommendation: Use constants:

const OG_IMAGE_WIDTH = 1200
const OG_IMAGE_HEIGHT = 630
const OG_TRANSFORM = `c_fill,w_${OG_IMAGE_WIDTH},h_${OG_IMAGE_HEIGHT}`

5. Performance: Network Requests in Validation (metadata/validate-metadata.ts:254)

External images are fetched during validation, which could be slow or fail due to network issues:

const response = await fetch(imageUrl)

Recommendation:

  • Add timeout to fetch requests
  • Consider mocking/skipping external image validation in CI
  • Or add retry logic with exponential backoff

6. Type Safety: Missing Return Type (app/(prose)/[slug]/page.tsx:24)

function generateJsonLd(post: PostType, slug: string) {

Recommendation: Add explicit return type for better type safety:

import { WithContext, Article } from 'schema-dts'

function generateJsonLd(post: PostType, slug: string): WithContext<Article> {

Low Priority

7. Code Consistency: Mixed Approaches (metadata/validate-metadata.ts)

Some validations throw errors, others push to the errors array:

throw new Error(`Invalid data: ${formatValidationError(result.error)}`)  // Some places
errors.push({ page: pageName, error: '...' })  // Other places

Recommendation: Stick to one pattern throughout (the errors array approach is better here).

8. Documentation: Missing JSDoc (utils/metadata.ts)

The constants file lacks JSDoc for some exports:

export const SITE_NAME = 'Michael Uloth'
export const SITE_DESCRIPTION = '...'
export const SITE_AUTHOR = 'Michael Uloth'

Recommendation: Add JSDoc descriptions explaining usage context.

9. Testing: Missing Edge Cases (io/cloudinary/ogImageTransforms.test.ts)

Missing tests for:

  • URLs with multiple /upload/ segments (edge case)
  • URLs with special characters
  • Empty strings

10. Maintenance: Hardcoded Twitter Handle (app/(prose)/[slug]/page.tsx:77)

twitter: {
  creator: '@ooloth',
}

Recommendation: Move to metadata constants file for consistency:

// utils/metadata.ts
export const TWITTER_HANDLE = '@ooloth'

🎯 Best Practices Followed

Proper error handling with Result types
Comprehensive test coverage (444 passing tests)
Clear commit history with descriptive messages
CI integration for automated validation
Type safety with TypeScript
Responsive to feedback (issue tracking in Beads)
Documentation in code and PR description


🔐 Security Assessment

Overall: Low risk, with one XSS concern that should be addressed.


⚡ Performance Considerations

Overall: Good performance, minor optimization opportunities.

  • ✅ Static generation with generateStaticParams
  • ✅ Images served via Cloudinary CDN
  • ✅ Metadata generated at build time
  • ⚠️ Network requests in validation could slow CI (see issue Add Playwright MCP server for browser automation #5)
  • ✅ No runtime overhead

🧪 Test Coverage Assessment

Overall: Excellent coverage with comprehensive validation.

  • ✅ Unit tests for all utilities
  • ✅ Component tests for metadata generation
  • ✅ Build-time integration tests
  • ✅ Edge cases covered (missing images, null descriptions)
  • ⚠️ Could add more edge cases for transforms (see issue Inject API client dependencies for easier testing #9)

📋 Final Recommendation

Approval Status:Approve with minor changes

This is high-quality work that significantly improves the site's social sharing capabilities. The implementation is solid, well-tested, and follows repository conventions.

Required before merge:

  1. Fix the JSON-LD XSS vulnerability (High Priority Cache cloudinary image metadata in file system during development #1)
  2. Add input validation to the metadata validation script (High Priority Cache Notion data in filesystem in local development #2)

Recommended follow-ups (can be separate PRs):

Great work! 🎉

Security & Validation:
- Fix XSS vulnerability in JSON-LD by escaping < characters
- Add input validation for og:image URLs in metadata validator
- Add 10-second timeout to external image fetches

Code Quality:
- Extract getPostUrl helper to eliminate duplicate URL construction
- Replace magic numbers with named constants (OG_IMAGE_WIDTH/HEIGHT)
- Add explicit return types to all functions
- Add comprehensive JSDoc comments to metadata constants

Consistency:
- Move Twitter handle to TWITTER_HANDLE constant across all pages

Testing:
- Add edge case tests for transformCloudinaryForOG
- Test empty strings, missing /upload/, encoded chars, query params

All changes address issues identified in PR review #17
- Extract getPostUrl helper in article page
- Add JsonLdArticle type definition
- Fix XSS vulnerability in JSON-LD script
- Move Twitter handle to constant in blog pages
…fetches

Performance improvements:
- Validate ALL 37 blog posts instead of just 1 example
- Skip network fetches for Cloudinary images (dimensions validated from URL)
- Cloudinary transformations are deterministic - trust URL parameters

Results:
- 10x more comprehensive (40 pages vs 4)
- 2-3x faster (1.6s vs 3-5s)
- Zero network calls to Cloudinary
- Only reads local files from out/ directory
@ooloth ooloth merged commit ab6d44a into main Dec 21, 2025
5 checks passed
@ooloth ooloth deleted the improve-metadata branch December 21, 2025 05:47
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.

2 participants