SEO og 업데이트: 단일 페이지 앱의 메타 태그 즉석에서

24364 단어 lambdacdkseoreact
내 React JS 단일 페이지 앱의 진입점은 https://www.wisaw.com입니다.

앱에서 다른 링크와 버튼을 클릭하여 앱을 탐색할 수 있습니다. 브라우저 URL 표시줄이 올바르게 업데이트되고 SEO에 필요한 메타 태그도 헬멧이라는 정말 멋진 반응 모듈을 통해 동적으로 적절하게 업데이트됩니다.

https://www.wisaw.com/photos/23377 으로 이동하면 개발자 콘솔에서 메타 태그가 올바르게 업데이트되는 것을 볼 수 있지만 해당 URL의 페이지 소스를 보면 항상 사용된 기본 index.html 파일의 내용이 표시됩니다. 반응 앱을 부트스트랩합니다.

브라우저에 URLhttps://www.wisaw.com/photos/23377을 입력하고 Enter 키를 누르면 해당 URL을 s3 버킷의 특정 리소스에 매핑할 수 없으므로 index.html로 리디렉션되며 404 대신 강제로 200 http 응답을 반환합니다(내 CloudFront 배포가 구성된 방식 -- 아래 참조). 따라서 여전히 index.html을 로드하여 React 앱을 본문에 지정된 유일한 div 태그로 부트스트랩한 다음 @react-navigation을 사용하여 요청된 URL에 해당하는 적절한 경로를 렌더링합니다.

결론 -- index.html은 요청하는 URL에 관계없이 항상 페이지 소스로 표시되는 정적 파일입니다(개발자 도구에서 검사할 수 있는 동적 DOM과 혼동하지 말 것). 애플리케이션 링크 또는/및 버튼을 따르거나 브라우저의 URL 표시줄에 링크를 입력합니다.

오늘날 대부분의 검색 엔진 크롤러 봇은 일반적으로 SPA의 동적 특성을 존중하기 위해 JavaScript를 실행합니다. 그러나 소셜 미디어 사이트(Twitter, FaceBook, LinkedIn) 중 하나에 https://www.wisaw.com/photos/23377과 같은 링크를 게시하거나 SMS를 통해 친구와 공유하면 html 소스에서 OG 태그를 찾고 찾을 수 없습니다. 모든 OG 태그(index.html은 정적임을 기억하십시오), 이미지 미리보기를 렌더링하지 않습니다.

가장 먼저 떠오르는 점은 요청에 사용할 수 있는 URL 문자열이 있다는 것입니다. 어떻게든 HTTP 요청을 가로채고 컨텍스트에 따라 응답 본문에 OG 태그를 동적으로 삽입할 수 있다면 작동해야 합니다.

이것이 바로 아래 솔루션에서 설명하려는 내용입니다.

먼저 CDK에서 필요한 요소를 정의하는 방법을 살펴보겠습니다(작동 방식을 설명하는 인라인 주석 읽기).

//  describing the bucket which hosts the react SPA code
      const webAppBucket =
                  s3.Bucket.fromBucketName(
                    this,
                    `wisaw-client`,
                    `wisaw-client`
                  )
      webAppBucket.grantPut(generateSiteMap_LambdaFunction)
      webAppBucket.grantPutAcl(generateSiteMap_LambdaFunction)


// lambda@edge function for ingecting OG meta tags on the fly
      const injectMetaTagsLambdaFunction =      
      new cloudfront.experimental.EdgeFunction(
        this,
        `${deployEnv()}_injectMetaTagsLambdaFunction`,
        {
// let's pick the latest runtime available
                  runtime: lambda.Runtime.NODEJS_16_X, 
                  code: lambda.Code.fromAsset(path.join(__dirname, '../lambda-fns/lambdas/injectMetaTagsLambdaFunction')),
                  handler: 'index.main',
// the max memory size for Lambda Edge function is 128 MB,
// which is significantly lower than for regular Lambda function
// Hopefully this will not make my lambda function to execute on 
// the lower end hardware,  
// and will still allocate fastest infrastructure -- I want 
// my Lambda Edge to be As Fast as Possible and not introduce 
// too much latency                   
                  memorySize: 128,                  
// The lambda Edge max timeout is 5 sec (unlike in regular Lambda), 
// which is good -- we do not want our Lambda Edge to ever 
// become a bottleneck for the entire system                   
                  timeout: cdk.Duration.seconds(5),
// logRetention is declared like this: 
// const logRetention = logs.RetentionDays.TWO_WEEKS                  
                  logRetention,
        }
      )

// Origin access identity for cloudfront to access the bucket
      const myCdnOai = 
        new cloudfront.OriginAccessIdentity(this, "CdnOai");
      webAppBucket.grantRead(myCdnOai);

// Describing the CloudFrontWebDistribution -- remember 
// to add the proper CNAME to your DNS when you 
// create a new CloudFrontWebDistribution.
// I do it manually, but you can probably figure out how
// to script in in CDK, especially if you are using Route53
      new cloudfront.CloudFrontWebDistribution
        (this, "wisaw-distro", {        
        originConfigs: [
          {
// this CloudFrontWebDistribution works with the bucket 
// where we deploy our react app code
            s3OriginSource: {
              s3BucketSource: webAppBucket,
              originAccessIdentity: myCdnOai,
            },
            behaviors: [
              {
// see errorConfigurations down below which will define 
// the default behavior
                isDefaultBehavior: true,
                compress: true,
              },
              {
// for any request that matches the /photos/* pattern, 
// it will use the following definition
                pathPattern: 'photos/*',
                compress: true,
                allowedMethods: 
                  cloudfront.CloudFrontAllowedMethods.ALL,
                minTtl: cdk.Duration.days(10),
                maxTtl: cdk.Duration.days(10),
                defaultTtl: cdk.Duration.days(10),
                forwardedValues: {
                  queryString: true,
                  cookies: {
                    forward: 'all'
                  }
                },
// this is the function which will execute for this pathPattern
                lambdaFunctionAssociations: [
                  {
// it will invoke the function during 
// cloudfront.LambdaEdgeEventType.VIEWER_REQUEST lifecycle stage                  
                  eventType: 
                    cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
// see the function source code down below
                  lambdaFunction: injectMetaTagsLambdaFunction,       
                  includeBody: true, // it really does not matter    
                  }, 
                ]
              }
            ],            
          }, 
        ],
        aliasConfiguration: {
          acmCertRef: "arn:aws:acm:us-east-1:963958500685:certificate/538e85e0-39f4-4d34-8580-86e8729e2c3c", 
// our CloudFrontWebDistribution will be attached to our app url
          names: ["www.wisaw.com"]
        },
        errorConfigurations: [ 
          {
            errorCode: 403, 
            responseCode: 200,
            errorCachingMinTtl: 31536000,
            responsePagePath: "/index.html"
          },
          {
// when we request like https://www.wisaw.com/search/Leaf, 
// it will respond with index.html and will forcefully return 200
            errorCode: 404, 
            responseCode: 200,
            errorCachingMinTtl: 31536000,
            responsePagePath: "/index.html"
          }

        ],
      })


이제 Lambda@Edge 함수가 어떻게 생겼는지 살펴보겠습니다.

// entry point
// the function is very light weight, it does not import any
// external packages, it supposed to add minimal latency
// to our request/response loop
export async function main
  (event: any = {}, context: any, callback: any) {
// console.log({event: JSON.stringify(event)})
  const { request} = event.Records[0].cf
// let's scrape image identifier from the url  
  const imageId = request.uri.replace('/photos/', '')

  console.log({imageId})
// the following line is a copy/paste from the index.html 
// deployed to the s3 bucket. We could read it dynamically,
// but the goal is to make this function as fast as possible.
// The original index.html file for react SPA does not change
// often if ever. As such, we can safely use a clone of it.
  const index = 
// don't forget to escape \! -- that's the only modification
// that needs to be applied to the minified index.html 
`
<\!doctype html><html lang="en" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/xhtml" xmlns:fb="http://ogp.me/ns/fb#"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="google-site-verification" content="RQGZzEN0xtT0w38pKeQ1L8u8P6dn7zxfu03jt0LGgF4"/><link rel="preconnect" href="https://www.wisaw.com"/><link rel="preconnect" href="https://s3.amazonaws.com"/><link rel="manifest" href="/manifest.json"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.webp"/><link rel="icon" type="image/webp" href="/favicon-32x32.webp" sizes="32x32"/><link rel="icon" type="image/webp" href="/favicon-16x16.webp" sizes="16x16"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"/><meta name="theme-color" content="#ffffff"/><link rel="preload" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" as="style" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous" onload='this.onload=null,this.rel="stylesheet"'/><script defer="defer" src="/static/js/main.8ee2345d.js"></script><link href="/static/css/main.e548762f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
`
// let's add the context specific meta tags to the <head>
// this should be self explanatory
  const body =  index.replace('<head>', 
  `<head>
    <meta name="image" property="og:image" content="https://wisaw-img-prod.s3.amazonaws.com/${imageId}" />
    <meta name="description" property="og:description" content="Check out What I saw Today" />
    <meta property="og:title" content="wisaw photo ${imageId}" />
    <meta property="og:url" content="https://www.wisaw.com/photos/${imageId}" />
    <meta property="og:site_name" content="wisaw.com" />
    <link rel="canonical" href="https://www.wisaw.com/photos/${imageId}" />
    <meta name="twitter:title" content="wisaw (What I Saw) photo ${imageId}" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:image" content="https://wisaw-img-prod.s3.amazonaws.com/${imageId}" />
`
  )
// let's define the response object
  const response = {
    status: '200',
    statusDescription: 'OK',
    headers: {
        'cache-control': [{
            key: 'Cache-Control',
            value: 'max-age=100'
        }],
        'content-type': [{
            key: 'Content-Type',
            value: 'text/html'
        }]
    },
    body,
  }
// and return it 
  callback(null, response)
}


그게 다야!

다음을 사용하여 솔루션을 테스트해야 합니다.

LinkedIn 포스트 인스펙터 --

Facebook 공유 디버거 -- https://developers.facebook.com/tools/debug/

Twitter 카드 유효성 검사기 --

전체 코드는 내 공개 github 저장소에서 찾을 수 있습니다 -- https://github.com/echowaves/WiSaw.cdk

CDK 스택 정의 -- https://github.com/echowaves/WiSaw.cdk/blob/main/lib/wi_saw.cdk-stack.ts

그리고 Lambda@Edge 함수 -- https://github.com/echowaves/WiSaw.cdk/blob/main/lambda-fns/lambdas/injectMetaTagsLambdaFunction/index.ts

즐거운 코딩 되세요...

기사는 원래 여기에 게시되었습니다https://www.echowaves.com/post/updating-seo-og-meta-tags-in-single-page-apps-on-the-fly.

좋은 웹페이지 즐겨찾기