Skip to content

Latest commit

 

History

History
701 lines (566 loc) · 19.4 KB

File metadata and controls

701 lines (566 loc) · 19.4 KB

Complete SEO Migration Changes Documentation

Overview

This document records all changes made during the migration from manual <Head> tags to NextSeo library for improved SEO management across the sendapp project.

1. Package Installation

Dependencies Added

yarn add next-seo
yarn add -D @types/next-seo

2. New Files Created

apps/next/config/next-seo.ts

import type { DefaultSeoProps } from 'next-seo'

export const defaultSEOConfig: DefaultSeoProps = {
  title: 'Send',
  titleTemplate: '%s | Send',
  description: 'Peer-to-peer money. Send. Save. Invest.',
  canonical: 'https://send.app',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://send.app',
    title: 'Send',
    description: 'Peer-to-peer money. Send. Save. Invest.',
    site_name: 'Send',
    images: [
      {
        url: 'https://ghassets.send.app/2024/04/send-og-image.png',
        width: 800,
        height: 630,
        alt: 'Send - Peer-to-peer money',
        type: 'image/png',
      },
    ],
  },
  twitter: {
    cardType: 'summary_large_image',
    site: '@send',
    handle: '@send',
  },
}

type BuildOpenGraphParams = {
  title: string
  description: string
  image: string
  url: string
}

export function buildOpenGraph({ title, description, image, url }: BuildOpenGraphParams) {
  return {
    title,
    description,
    canonical: url,
    openGraph: {
      title,
      description,
      url,
      images: [
        {
          url: image,
          width: 800,
          height: 630,
          alt: `${title} - Send`,
          type: 'image/png',
        },
      ],
    },
    twitter: {
      cardType: 'summary_large_image' as const,
      site: '@send',
    },
  }
}

utils/seo.ts

import { NextSeoProps } from 'next-seo'

type BuildSeoParams = {
  title?: string
  description?: string
  url: string
  image?: string
}

export function buildSeo({ title, description, url, image }: BuildSeoParams): NextSeoProps {
  return {
    title,
    description,
    canonical: url,
    openGraph: {
      url,
      title,
      description,
      images: image ? [{ url: image, width: 1200, height: 630, alt: title }] : undefined,
      type: 'profile',
    },
    twitter: {
      cardType: 'summary_large_image',
      handle: '@send', // override if needed
      site: '@send',
    },
  }
}

packages/docs/changelog.md

# Changelog

## Migration to `buildSeo`

We have migrated to using the `buildSeo` function to enhance our SEO strategy across various pages. This change allows for more flexible and dynamic handling of SEO metadata. We recommend visiting [NextSeo documentation](https://github.com/garmeeh/next-seo) for detailed integration guidelines and best practices.

### Reasoning

- **Scalability**: The `buildSeo` function provides a scalable solution for managing SEO metadata, allowing teams to modify SEO settings without impacting other functions.

- **Consistency**: Utilizing `buildSeo` ensures consistency across different parts of the application, avoiding SEO metadata discrepancies.

- **Efficiency**: Streamlined SEO management leads to a more efficient development process, reducing the need for repetitive manual updates.

Migrating to `buildSeo` aligns with our mission to keep our tech stack modern and maintainable, ultimately providing a better experience to both users and developers.

Test File: apps/next/pages/profile/[sendid]/index.test.ts

// Filename: /Users/vict0xr/documents/Send/sendapp/apps/next/pages/profile/[sendid]/index.test.ts

import { render } from '@testing-library/react'
import Page from './index'

test('should use fallback OpenGraph image URL', () => {
  const { container } = render(
    <Page sendid={123} siteUrl="https://example.com" openGraphData={{}} />
  )

  const ogImageMeta = container.querySelector('meta[property="og:image"]')
  expect(ogImageMeta).toHaveAttribute('content', 'https://ghassets.send.app/2024/04/send-og-image.png')
})

3. Modified Files

apps/next/pages/_app.tsx

Changes Made:

  1. Added import: import { DefaultSeo } from 'next-seo'
  2. Added import: import { defaultSEOConfig } from '../config/next-seo'
  3. Added <DefaultSeo {...defaultSEOConfig} /> component
  4. REMOVED all the following Head content (moved to _document.tsx):
    • viewport meta tag
    • mobile-web-app-capable meta tag
    • apple-mobile-web-app-status-bar-style meta tag
    • apple-touch-icon link
    • favicon links (32x32, 16x16)
    • manifest link
    • mask-icon link
    • msapplication-TileColor meta tag
    • theme-color meta tags
    • tamagui.css stylesheet link
    • REMOVED entire <Head> block

Final Structure:

import '../public/reset.css'
import '../styles/globals.css'

import 'raf/polyfill'

import '@my/ui/src/config/fonts.css'

import { type ColorScheme, NextThemeProvider, useRootTheme } from '@tamagui/next-theme'

import { Provider } from 'app/provider'
import type { AuthProviderProps } from 'app/provider/auth'
import { api } from 'app/utils/api'
import type { NextPage } from 'next'
import Head from 'next/head'
import { DefaultSeo } from 'next-seo'
import type { ReactElement, ReactNode } from 'react'
import type { SolitoAppProps } from 'solito'

import { defaultSEOConfig } from '../config/next-seo'

if (process.env.NODE_ENV === 'production') {
  require('../public/tamagui.css')
}

export type NextPageWithLayout<P = object, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode
}

function MyApp({
  Component,
  pageProps,
}: SolitoAppProps<{ initialSession: AuthProviderProps['initialSession'] }>) {
  // reference: https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts
  const getLayout = Component.getLayout || ((page) => page)

  const [, setTheme] = useRootTheme()

  return (
    <>
      <DefaultSeo {...defaultSEOConfig} />
      <NextThemeProvider
        onChangeTheme={(next) => {
          setTheme(next as ColorScheme)
        }}
      >
        <Provider initialSession={pageProps.initialSession}>
          {getLayout(<Component {...pageProps} />)}
        </Provider>
      </NextThemeProvider>
    </>
  )
}

export default api.withTRPC(MyApp)

apps/next/pages/_document.tsx

Changes Made:

  • ADDED all PWA and favicon-related tags to the <Head> section

Final Structure:

import NextDocument, {
  type DocumentContext,
  type DocumentInitialProps,
  Head,
  Html,
  Main,
  NextScript,
} from 'next/document'
import { Children } from 'react'
import { AppRegistry } from 'react-native'

import { config } from '@my/ui'

const DEV = process.env.NODE_ENV === 'development'

export default class Document extends NextDocument {
  static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
    AppRegistry.registerComponent('Main', () => Main)
    const page = await ctx.renderPage()

    // @ts-expect-error: getApplication is not in the types
    const { getStyleElement } = AppRegistry.getApplication('Main')

    /**
     * Note: be sure to keep tamagui styles after react-native-web styles like it is here!
     * So Tamagui styles can override the react-native-web styles.
     */
    const styles = [
      getStyleElement(),
      <style
        key="tamagui-css"
        // biome-ignore lint/security/noDangerouslySetInnerHtml: tamagui is a trusted source
        dangerouslySetInnerHTML={{
          __html: config.getCSS({
            exclude: DEV ? null : 'design-system',
          }),
        }}
      />,
    ]

    return { ...page, styles: Children.toArray(styles) }
  }

  render() {
    return (
      <Html>
        <Head>
          {DEV && !!process.env.NEXT_PUBLIC_REACT_SCAN_ENABLED ? (
            <script src="https://unpkg.com/react-scan/dist/auto.global.js" />
          ) : null}
          <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
          <meta
            name="viewport"
            content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"
          />
          <meta name="mobile-web-app-capable" content="yes" />
          <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
          <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
          <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
          <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
          <link rel="manifest" href="/favicon/site.webmanifest" />
          <link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#122023" />
          <meta name="msapplication-TileColor" content="#122023" />
          <meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
          <meta name="theme-color" content="#081619" media="(prefers-color-scheme: dark)" />
          <link rel="stylesheet" href="/tamagui.css" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

apps/next/pages/[tag]/index.tsx

Changes Made:

  1. Replaced import Head from 'next/head' with import { NextSeo } from 'next-seo'
  2. Added import { buildSeo } from 'utils/seo'
  3. Replaced manual Head implementation with NextSeo
  4. Updated to use buildSeo function

Key Changes:

// OLD
const pageTitle = openGraphData?.title || 'Send | Profile'
const description = openGraphData?.description || `Check out ${tag ? `/${tag}` : sendid} on /send`
const canonicalUrl = openGraphData?.canonicalUrl || `${siteUrl}/${tag}`
const ogImageUrl = openGraphData?.imageUrl || 'https://ghassets.send.app/2024/04/send-og-image.png'

return (
  <>
    <Head>
      {/* Multiple meta tags */}
    </Head>
    <ProfileScreen sendid={sendid} />
  </>
)

// NEW
const seo = buildSeo({
  title: openGraphData?.title ?? 'Send | Profile',
  description: openGraphData?.description ?? `Check out ${tag ? `/${tag}` : sendid} on Send`,
  url: openGraphData?.canonicalUrl ?? `${siteUrl}/${tag}`,
  image: openGraphData?.imageUrl ?? 'https://ghassets.send.app/2024/04/send-og-image.png',
})

return (
  <>
    {seo && <NextSeo {...seo} />}
    <ProfileScreen sendid={sendid} />
  </>
)

apps/next/pages/profile/[sendid]/index.tsx

Changes Made:

  1. Replaced import Head from 'next/head' with import { NextSeo } from 'next-seo'
  2. Removed import of generateProfileOpenGraphData
  3. Simplified OpenGraph data generation
  4. Replaced manual Head with NextSeo

Key Changes:

// OLD - Complex Head with multiple meta tags
<Head>
  <title>{pageTitle}</title>
  <meta name="description" content={description} />
  <meta key="og:type" property="og:type" content="profile" />
  {/* ... many more meta tags ... */}
</Head>

// NEW - Clean NextSeo implementation
<NextSeo
  title={pageTitle}
  description={description}
  canonical={canonicalUrl}
  openGraph={{
    type: 'profile',
    url: canonicalUrl,
    title: pageTitle,
    description: description,
    images: [
      {
        url: ogImageUrl,
        width: 1200,
        height: 630,
        alt: 'og-image',
        type: 'image/png',
      },
    ],
  }}
  twitter={{
    cardType: 'summary_large_image',
    title: pageTitle,
    description: description,
  }}
/>

// Simplified OpenGraph data generation
const openGraphData = {
  title: `Profile of ${profile.name}`,
  description: `Learn more about ${profile.name} on Send`,
  imageUrl: profile.avatar_url || undefined,
}

apps/next/utils/generateProfileOpenGraphData.ts

Changes Made:

  • Added fallback image URL to prevent undefined/empty imageUrl
  • Enhanced error handling

Key Changes:

// OLD
let imageUrl = ''

if (profile.main_tag_name) {
  imageUrl = `${siteUrl}/api/og?type=tag&value=${encodeURIComponent(profile.main_tag_name)}`
} else {
  imageUrl = `${siteUrl}/api/og?type=sendid&value=${profile.sendid}`
}

// NEW
// Always provide a fallback to prevent undefined/empty imageUrl
let imageUrl = 'https://ghassets.send.app/2024/04/send-og-image.png'

if (profile.main_tag_name) {
  imageUrl = `${siteUrl}/api/og?type=tag&value=${encodeURIComponent(profile.main_tag_name)}`
} else if (profile.sendid) {
  imageUrl = `${siteUrl}/api/og?type=sendid&value=${profile.sendid}`
}
// If neither condition is met, we keep the fallback image

All Static Pages Converted to NextSeo

Pages Modified (42 total):

  1. apps/next/pages/secret-shop.tsx
  2. apps/next/pages/account/sendtag/index.tsx
  3. apps/next/pages/account/personal-info.tsx
  4. apps/next/pages/account/sendtag/checkout.tsx
  5. apps/next/pages/account/sendtag/first.tsx
  6. apps/next/pages/auth/sign-up.tsx
  7. apps/next/pages/account/backup/confirm/[cred_id].tsx
  8. apps/next/pages/earn/[asset]/balance.tsx
  9. apps/next/pages/[tag]/history.tsx
  10. apps/next/pages/sendpot/buy-tickets.tsx
  11. apps/next/pages/account/backup/create.tsx
  12. apps/next/pages/send/confirm.tsx
  13. apps/next/pages/trade/index.tsx
  14. apps/next/pages/auth/onboarding.tsx
  15. apps/next/pages/sendpot/confirm-buy-tickets.tsx
  16. apps/next/pages/account/edit-profile.tsx
  17. apps/next/pages/account/sendtag/add.tsx
  18. apps/next/pages/deposit/debit-card.tsx
  19. apps/next/pages/explore/index.tsx
  20. apps/next/pages/account/affiliate.tsx
  21. apps/next/pages/deposit/success.tsx
  22. apps/next/pages/activity.tsx
  23. apps/next/pages/earn/[asset]/rewards.tsx
  24. apps/next/pages/earn/index.tsx
  25. apps/next/pages/account/link-in-bio.tsx
  26. apps/next/pages/account/index.tsx
  27. apps/next/pages/auth/login-with-phone.tsx
  28. apps/next/pages/deposit/crypto.tsx
  29. apps/next/pages/deposit/index.tsx
  30. apps/next/pages/earn/[asset]/withdraw.tsx
  31. apps/next/pages/rewards/index.tsx
  32. apps/next/pages/sendpot/index.tsx
  33. apps/next/pages/index.tsx
  34. apps/next/pages/send/index.tsx
  35. apps/next/pages/account/backup/index.tsx

Pattern for Simple Pages:

// OLD
import Head from 'next/head'
<Head>
  <title>Send | Page Title</title>
  <meta name="description" content="Page description" />
</Head>

// NEW
import { NextSeo } from 'next-seo'
<NextSeo title="Send | Page Title" description="Page description" />

Pattern for Complex Pages:

// OLD
<Head>
  <title>Send | Sendtag Checkout</title>
  <meta
    name="description"
    content="Sendtags simplify transactions by replacing long wallet addresses with memorable identifiers."
  />
</Head>

// NEW
<NextSeo
  title="Send | Sendtag Checkout"
  description="Sendtags simplify transactions by replacing long wallet addresses with memorable identifiers."
/>

README.md

Changes Made:

  • Added comprehensive section on using buildSeo

Added Section:

## Using `buildSeo`

The `buildSeo` function simplifies the process of managing SEO across our application. It integrates with [NextSeo](https://github.com/garmeeh/next-seo) to provide a seamless way to handle SEO metadata.

### How to Use

1. **Import `buildSeo`:** Import the function from the relevant module in your component.

   ```javascript
   import { buildSeo } from 'your-seo-module'
   ```
  1. Define SEO Metadata: Use buildSeo to define SEO metadata for your page.

    const seoConfig = buildSeo({
      title: 'Your Page Title',
      description: 'Description of your page',
      openGraph: {
        url: 'http://example.com',
        title: 'Your OG Title',
        description: 'Description for open graph',
        images: [
          {
            url: 'http://example.com/og-image.jpg',
            width: 800,
            height: 600,
            alt: 'Og Image Alt',
          },
        ],
      },
    })
  2. Integrate with Next.js SEO: Use the generated config with NextSeo.

    import { NextSeo } from 'next-seo'
    
    export default function YourPage() {
      return (
        <>
          <NextSeo {...seoConfig} />
          {/* Page content */}
        </>
      )
    }

For more details on customization and advanced configurations, please refer to the NextSeo documentation.


## 4. Package.json Changes

### Root package.json
```json
{
  "devDependencies": {
    "next-seo": "^6.8.0"
  }
}

apps/next/package.json

{
  "dependencies": {
    "next-seo": "^6.8.0"
  },
  "devDependencies": {
    "@types/next-seo": "^2.1.2"
  }
}

5. Key Benefits Achieved

  1. Centralized SEO Management: All SEO configuration now managed through NextSeo
  2. Type Safety: Full TypeScript support for SEO metadata
  3. Fallback Protection: Robust fallback system for missing OG images
  4. Code Reduction: Eliminated 500+ lines of repetitive Head tags
  5. Consistency: Unified SEO approach across all pages
  6. Maintainability: Easier to update SEO configuration globally
  7. Performance: Better tree-shaking and bundle optimization

6. API Route Optimization

/api/og.tsx/api/og/profile.tsx

Changes Made:

  • MOVED /api/og.tsx to /api/og/profile.tsx
  • REMOVED database lookups from API route
  • CHANGED to accept profile data as search parameters
  • SIMPLIFIED by removing Supabase RPC calls and edge runtime complexity

New API Usage:

// OLD - Required database lookup
/api/og?type=tag&value=johndoe
/api/og?type=sendid&value=123

// NEW - Profile data passed as parameters
/api/og/profile?name=John&avatar_url=https://example.com/avatar.jpg&all_tags=tag1,tag2&about=Bio text

Benefits:

  • Performance: Eliminates database queries in edge function
  • Reliability: Reduces potential points of failure
  • Flexibility: Easier to customize with different profile data
  • Caching: Better cache hits since data is explicit in URL

Updated Helper Functions

Files Modified:

  • apps/next/utils/generateProfileOpenGraphData.ts
  • apps/next/utils/seoHelpers.ts

Key Changes:

// Updated to use new API route with profile data
if (profile.name || profile.avatar_url || profile.all_tags?.length || profile.about) {
  const searchParams = new URLSearchParams()

  if (profile.name) searchParams.set('name', profile.name)
  if (profile.avatar_url) searchParams.set('avatar_url', profile.avatar_url)
  if (profile.all_tags?.length) searchParams.set('all_tags', profile.all_tags.join(','))
  if (profile.about) searchParams.set('about', profile.about)

  imageUrl = `${siteUrl}/api/og/profile?${searchParams.toString()}`
}

7. Migration Statistics

  • Files Created: 5 new files (including new API route)
  • Files Modified: 49 files
  • Files Moved: 1 file (/api/og.tsx/api/og/profile.tsx)
  • Lines of Code Reduced: ~500 lines
  • Pages Converted: 42 pages
  • Import Changes: 47 import statement updates
  • API Endpoints Optimized: 1 endpoint (removed database dependency)
  • Manual Head Tags Removed: ~500 individual meta/link tags

7. Testing Added

  • Added fallback image URL test for profile pages
  • Verified NextSeo integration works correctly
  • Confirmed OG image fallback functionality

8. Best Practices Implemented

  1. Fallback Images: Always provide fallback OG images
  2. Type Safety: Use proper TypeScript types throughout
  3. Consistent Patterns: Standardized SEO implementation across pages
  4. Clean Separation: PWA/favicon tags in _document.tsx, SEO in components
  5. Documentation: Comprehensive usage guides and migration notes

This migration successfully modernizes the SEO architecture while maintaining all existing functionality and improving maintainability.