Conversation
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)
There was a problem hiding this comment.
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.tsfor 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.
Pull Request Review: Add comprehensive social sharing metadataThis 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: ✅ Strengths1. Excellent Test Coverage
2. Well-Organized Code
3. Robust Implementation
4. Developer Experience
🔍 Issues & RecommendationsHigh Priority1. Security: XSS Risk in JSON-LD (app/(prose)/[slug]/page.tsx:98)<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />Issue: While Recommendation: Add explicit escaping: <script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, '\\u003c')
}}
/>Or use a dedicated library like 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 Priority3. 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 53Recommendation: 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:
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 Priority7. 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 placesRecommendation: 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:
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 🔐 Security AssessmentOverall: Low risk, with one XSS concern that should be addressed.
⚡ Performance ConsiderationsOverall: Good performance, minor optimization opportunities.
🧪 Test Coverage AssessmentOverall: Excellent coverage with comprehensive validation.
📋 Final RecommendationApproval 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:
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
✅ What
🤔 Why
👩🔬 How to validate
npm run build- Expect build to succeed with metadata validation passingout/index.html- Expect to seeog:title,og:image,og:url,twitter:cardmeta tagsout/[post-slug]/index.html- Expect to see article-specific metadata includingarticle:published_timeand JSON-LD scriptw_1200,h_630in the transformation params