SEO og 업데이트: 단일 페이지 앱의 메타 태그 즉석에서
앱에서 다른 링크와 버튼을 클릭하여 앱을 탐색할 수 있습니다. 브라우저 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.
Reference
이 문제에 관하여(SEO og 업데이트: 단일 페이지 앱의 메타 태그 즉석에서), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/dmitryame/updating-seo-og-meta-tags-in-single-page-apps-on-the-fly-5bcg텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)