'Next.js | How to improve seo when doing dynamic routing with ssr
I am writing dynamic routes code in, like the code below. So my question is, this dynamic routes page is not reflected in google at all. I have dynamically set the title of this page, but when I search for that title in google, it doesn't show up. What should I do?
src/pages/salon/[[...salon]].js (abridged edition)
import React from 'react'
// Firebase
import { db } from "../../../firebaseConfig"
import { doc, getDoc, updateDoc, arrayUnion, arrayRemove } from "firebase/firestore"
import { getAuth, onAuthStateChanged } from "firebase/auth"
const Salon = ({ id, tag, data }) => {
return (
<>
<NextSeo
title={data.title}
description={data.description}
url={data.url}
canonical={data.url}
openGraph={{
url: data.url,
title: data.title,
description: data.description,
type: "article",
images: [
{
url: data.mainImages[1],
width: 800,
height: 600,
alt: "Image",
}],
}}
/>
<div>{data.salonName}【{data.salonNameKana}】</div>
</>
)
}
export default Salon
export async function getServerSideProps(context) {
const url = context.params.salon
const salonRef = doc(db, "salons", url[0])
const salonSnap = await getDoc(salonRef)
const data = salonSnap.data()
if (!data) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}
return {
props: {
id: url[0],
tag: url[1] ? url[1] : '',
data: data
}
};
}
src/pages/salon/[[...salon]].js (full edition)
import React, { useEffect } from "react"
import Link from 'next/link'
import { useRouter } from 'next/router'
import Image from "next/image"
// firebase関連
import { db } from "../../firebaseConfig"
import {
collection,
orderBy,
query,
getDocs,
limit,
where
} from 'firebase/firestore'
// fontawesome
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faMapMarkedAlt, faSubway } from "@fortawesome/free-solid-svg-icons"
// Redux関連
import { useSelector } from 'react-redux'
import { NextSeo } from "next-seo"
const Index = ({newSalonList}) => {
const router = useRouter()
const area = useSelector((state) => state.searchKey.area)
return (
<div className="Top">
<NextSeo />
{/* 検索 */}
<div className="search">
<div className="reserveText">イメコンサロンを検索する</div>
<div className="area">
<p>{area}</p>
<Link href="area">
<a><button>エリア変更</button></a>
</Link>
</div>
<div className="form">
<form action="/search" method="GET" className="search-box">
<input type="text" name="query" placeholder="サロン名・エリア・メニューなどから検索" />
<button type="submit" value=" 検索"><FontAwesomeIcon icon={faSearch} /></button>
</form>
</div>
<div className="common_relative_image mainImg">
<Image
src='/imecon-personal-color.jpeg'
alt="mainTopImg"
className="mainImg"
layout="fill"
objectFit="contain"
priority
/>
</div>
<div className="searchWay">
<button className="searchWayItem" onClick={(e) =>{
e.preventDefault()
router.push({
pathname: "/area",
// 検索する地域をクリックした後の遷移先のURLを渡す
query: {to:'area-search'}
})
}}>
<div className="areaImage common_relative_image">
<Image
src='/JapanMap.svg'
alt="japanMap"
layout="fill"
objectFit="contain"
/>
</div>
<p><span>エリア</span><br />から探す</p>
</button>
<Link href="station">
<a className="searchWayItem" style={{cursor: "default", background: "#D9D9D9"}} onClick={ (event) => event.preventDefault() }>
<FontAwesomeIcon icon={faSubway} />
<p><span>駅</span>から探す</p>
<small style={{color: "red"}}>coming soon</small>
</a>
</Link>
<Link href="location">
<a className="searchWayItem" style={{cursor: "default", background: "#D9D9D9"}} onClick={ (event) => event.preventDefault() }>
<FontAwesomeIcon icon={faMapMarkedAlt} />
<p><span>現在地</span><br />から探す</p>
<small style={{color: "red"}}>coming soon</small>
</a>
</Link>
</div>
</div>
{/* ブックマークから探す */}
<Link href="/bookmark">
<a className="bookMark">
<svg xmlns="http://www.w3.org/2000/svg" width="24" fill="#FF0100" height="24" viewBox="0 0 24 24"><path d="M12 4.248c-3.148-5.402-12-3.825-12 2.944 0 4.661 5.571 9.427 12 15.808 6.43-6.381 12-11.147 12-15.808 0-6.792-8.875-8.306-12-2.944z"/></svg>
ブックマークから探す
</a>
</Link>
{/* お知らせ */}
<div className="information">
<div className="common_title">| Information</div>
<ul>
<li>
<Link href="/tosalons">
<a>掲載をご希望のサロン様はこちら</a>
</Link>
</li>
</ul>
</div>
{/* 新着サロン */}
<div className="newSalon">
<div className="common_title">| 新着のサロン</div>
<ul>
{newSalonList.map((data) => {
return (
// 新着順のサロンを表示:useEffectで格納したデータ
<li className="newSalonMain" key={data.salonId}>
<Link href={'/salon/' + data.salonId}>
<a className="newSalonMainList">
<div className="newSalonMainListImage common_relative_image">
<Image
src={data.mainImageUrl}
alt="salonMainImage"
layout="fill"
objectFit="contain"
/>
</div>
<div className="newSalonMainListInfo">
<p className="salonName">{data.salonName}{data.salonNameKana}</p>
<div className="salonTraffic">
<div className="common_relative_image salonTrafficImg">
<Image
src='/traffic.svg'
alt="traffic"
layout="fill"
objectFit="contain"
/>
</div>
<span>{data.address}</span>
</div>
<div className="newSalonMainListInfoMenu">
<div className="newSalonMainListInfoMenuTitle">{data.title}</div>
<div className="newSalonMainListInfoMenuPrice">¥{data.price}</div>
</div>
</div>
</a>
</Link>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Index
export async function getServerSideProps() {
// firestoreから新着順サロンのドキュメントを5つ取得
const salons = query(collection(db, "salons"), where('publish', "==", true), orderBy('createAt', "desc"), limit(5))
const querySnapshot = await getDocs(salons)
// 一旦useEffect内で配列を作り、格納する。後でこの配列をuseStateに格納
let subSalonList = []
// 取得した新着順サロンのドキュメントから情報を取得し、subSalonListに格納
await querySnapshot.forEach((doc) => {
subSalonList = [...subSalonList, {
salonId: doc.id,
salonName: doc._document.data.value.mapValue.fields.salonName.stringValue,
salonNameKana: doc._document.data.value.mapValue.fields.salonNameKana ? '【' + doc._document.data.value.mapValue.fields.salonNameKana.stringValue + '】': '',
address: doc._document.data.value.mapValue.fields.access ? doc._document.data.value.mapValue.fields.access.stringValue: '',
mainImageUrl: doc._document.data.value.mapValue.fields.mainImages.mapValue.fields[1].stringValue,
}]
})
// サブコレクションの情報は↑で一緒に取得できなかったので、↓で格納する。subSalonListをループして、salonIdを元に順番に情報を取得し、subSalonListに格納している。
for (const value of subSalonList) {
try {
// salonIdを元にmenuサブコレクション下のドキュメントでtopがtrueのものを検索&取得
const q = query(collection(db, "salons", value.salonId, "menu"))
const querySnapshot2 = await getDocs(q)
value.title = querySnapshot2.docs[0]._document.data.value.mapValue.fields.title.stringValue
value.price = querySnapshot2.docs[0]._document.data.value.mapValue.fields.price.integerValue.toLocaleString()
value.time = querySnapshot2.docs[0]._document.data.value.mapValue.fields.time.integerValue.toLocaleString()
} catch(err) {
console.log(err)
return 'err'
}
}
return {
props: {
newSalonList: subSalonList,
}
}
}
site meta tag
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><link rel="icon" href="https://kanunu-beauty.com/favicon.ico">
<link rel="apple-touch-icon" href="https://kanunu-beauty.com/favicon.ico">
<title>Kanunu(カヌヌ)|パーソナルカラー・骨格診断・顔タイプ診断などのイメコンサロンが探せる日本最大級の検索サイト</title>
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@kanunu_official">
<meta name="twitter:creator" content="@kanunu_official">
<meta property="og:image:alt" content="Kanunu_Image">
<meta property="og:image:width" content="800">
<meta property="og:image:height" content="600">
<meta property="og:locale" content="ja_JP"><meta property="og:site_name" content="Kanunu(カヌヌ)"><link rel="icon" href="https://kanunu-beauty.com/favicon.ico"><link rel="apple-touch-icon" href="https://kanunu-beauty.com/favicon.ico" sizes="76x76"><meta name="robots" content="index,follow">
<meta property="og:image" content="https://kanunu-beauty.com/kanunu.png">
<meta name="description" content="パーソナルカラー・骨格診断・顔タイプ診断などのイメコンサロンが探せる日本最大級の検索サイト。24時間いつでもネット検索OK。イメージコンサルティングを検索するならKanunu">
<meta property="og:title" content="Kanunu(カヌヌ)|パーソナルカラー・骨格診断・顔タイプ診断などのイメコンサロンが探せる日本最大級の検索サイト">
<meta property="og:description" content="パーソナルカラー・骨格診断・顔タイプ診断などのイメコンサロンが探せる日本最大級の検索サイト。24時間いつでもネット検索OK。イメージコンサルティングを検索するならKanunu">
<meta property="og:url" content="https://kanunu-beauty.com/">
<meta property="og:type" content="website">
<link rel="canonical" href="https://kanunu-beauty.com/">
<link rel="preload" as="image" imagesrcset="/_next/image?url=%2Fimecon-personal-color.jpeg&w=640&q=75 640w, /_next/image?url=%2Fimecon-personal-color.jpeg&w=750&q=75 750w, /_next/image?url=%2Fimecon-personal-color.jpeg&w=828&q=75 828w, /_next/image?url=%2Fimecon-personal-color.jpeg&w=1080&q=75 1080w, /_next/image?url=%2Fimecon-personal-color.jpeg&w=1200&q=75 1200w, /_next/image?url=%2Fimecon-personal-color.jpeg&w=1920&q=75 1920w, /_next/image?url=%2Fimecon-personal-color.jpeg&w=2048&q=75 2048w, /_next/image?url=%2Fimecon-personal-color.jpeg&w=3840&q=75 3840w" imagesizes="100vw">
<meta name="next-head-count" content="24">
<meta charset="UTF-8">
<meta name="theme-color" content="#ff889e"><script src="https://apis.google.com/_/scs/abc-static/_/js/k=gapi.lb.ja.5EzdJRYYqVI.O/m=gapi_iframes/rt=j/sv=1/d=1/ed=1/rs=AHpOoo_YkgRfk9NA-tU81xmKmFrifcc8Yg/cb=gapi.loaded_0?le=scs" async=""></script><script src="https://partner.googleadservices.com/gampad/cookie.js?domain=kanunu-beauty.com&callback=_gfp_s_&client=ca-pub-3226715501424001&cookie=ID%3Da210217fc7bc19a0-226fbd731ed100ba%3AT%3D1648307385%3ART%3D1648307385%3AS%3DALNI_Mbl4zhbFc5KZH7kiBhF4bogpQHECQ"></script><script src="https://pagead2.googlesyndication.com/pagead/managed/js/adsense/m202205050101/show_ads_impl_fy2019.js?bust=31067451" id="google_shimpl"></script><script async="" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3226715501424001" crossorigin="anonymous" data-checked-head="true"></script><link rel="preload" href="/_next/static/css/3af99d7a8bc648e8.css" as="style"><link rel="stylesheet" href="/_next/static/css/3af99d7a8bc648e8.css" data-n-g=""><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-5cd94c89d3acac5f.js"></script><script src="/_next/static/chunks/webpack-5752944655d749a0.js" defer=""></script><script src="/_next/static/chunks/framework-5f4595e5518b5600.js" defer=""></script><script src="/_next/static/chunks/main-958eae9894028eb3.js" defer=""></script><script src="/_next/static/chunks/pages/_app-7066ec57ee9a283b.js" defer=""></script><script src="/_next/static/chunks/675-a0e7bc4ebaaecba0.js" defer=""></script><script src="/_next/static/chunks/pages/index-b8f101c1b5c4520a.js" defer=""></script><script src="/_next/static/c6OFukToh53vEUvjEggAL/_buildManifest.js" defer=""></script><script src="/_next/static/c6OFukToh53vEUvjEggAL/_ssgManifest.js" defer=""></script><script src="/_next/static/c6OFukToh53vEUvjEggAL/_middlewareManifest.js" defer=""></script>
<meta http-equiv="origin-trial" content="AxujKG9INjsZ8/gUq8+dTruNvk7RjZQ1oFhhgQbcTJKDnZfbzSTE81wvC2Hzaf3TW4avA76LTZEMdiedF1vIbA4AAABueyJvcmlnaW4iOiJodHRwczovL2ltYXNkay5nb29nbGVhcGlzLmNvbTo0NDMiLCJmZWF0dXJlIjoiVHJ1c3RUb2tlbnMiLCJleHBpcnkiOjE2NTI3NzQ0MDAsImlzVGhpcmRQYXJ0eSI6dHJ1ZX0=">
<meta http-equiv="origin-trial" content="Azuce85ORtSnWe1MZDTv68qpaW3iHyfL9YbLRy0cwcCZwVnePnOmkUJlG8HGikmOwhZU22dElCcfrfX2HhrBPAkAAAB7eyJvcmlnaW4iOiJodHRwczovL2RvdWJsZWNsaWNrLm5ldDo0NDMiLCJmZWF0dXJlIjoiVHJ1c3RUb2tlbnMiLCJleHBpcnkiOjE2NTI3NzQ0MDAsImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9">
<meta http-equiv="origin-trial" content="A16nvcdeoOAqrJcmjLRpl1I6f3McDD8EfofAYTt/P/H4/AWwB99nxiPp6kA0fXoiZav908Z8etuL16laFPUdfQsAAACBeyJvcmlnaW4iOiJodHRwczovL2dvb2dsZXRhZ3NlcnZpY2VzLmNvbTo0NDMiLCJmZWF0dXJlIjoiVHJ1c3RUb2tlbnMiLCJleHBpcnkiOjE2NTI3NzQ0MDAsImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9">
<meta http-equiv="origin-trial" content="AxBHdr0J44vFBQtZUqX9sjiqf5yWZ/OcHRcRMN3H9TH+t90V/j3ENW6C8+igBZFXMJ7G3Pr8Dd13632aLng42wgAAACBeyJvcmlnaW4iOiJodHRwczovL2dvb2dsZXN5bmRpY2F0aW9uLmNvbTo0NDMiLCJmZWF0dXJlIjoiVHJ1c3RUb2tlbnMiLCJleHBpcnkiOjE2NTI3NzQ0MDAsImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9">
<meta http-equiv="origin-trial" content="A88BWHFjcawUfKU3lIejLoryXoyjooBXLgWmGh+hNcqMK44cugvsI5YZbNarYvi3roc1fYbHA1AVbhAtuHZflgEAAAB2eyJvcmlnaW4iOiJodHRwczovL2dvb2dsZS5jb206NDQzIiwiZmVhdHVyZSI6IlRydXN0VG9rZW5zIiwiZXhwaXJ5IjoxNjUyNzc0NDAwLCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ==">
<meta http-equiv="origin-trial" content="AzoawhTRDevLR66Y6MROu167EDncFPBvcKOaQispTo9ouEt5LvcBjnRFqiAByRT+2cDHG1Yj4dXwpLeIhc98/gIAAACFeyJvcmlnaW4iOiJodHRwczovL2RvdWJsZWNsaWNrLm5ldDo0NDMiLCJmZWF0dXJlIjoiUHJpdmFjeVNhbmRib3hBZHNBUElzIiwiZXhwaXJ5IjoxNjYxMjk5MTk5LCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ==">
<meta http-equiv="origin-trial" content="A6+nc62kbJgC46ypOwRsNW6RkDn2x7tgRh0wp7jb3DtFF7oEhu1hhm4rdZHZ6zXvnKZLlYcBlQUImC4d3kKihAcAAACLeyJvcmlnaW4iOiJodHRwczovL2dvb2dsZXN5bmRpY2F0aW9uLmNvbTo0NDMiLCJmZWF0dXJlIjoiUHJpdmFjeVNhbmRib3hBZHNBUElzIiwiZXhwaXJ5IjoxNjYxMjk5MTk5LCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ==">
<meta http-equiv="origin-trial" content="A/9La288e7MDEU2ifusFnMg1C2Ij6uoa/Z/ylwJIXSsWfK37oESIPbxbt4IU86OGqDEPnNVruUiMjfKo65H/CQwAAACLeyJvcmlnaW4iOiJodHRwczovL2dvb2dsZXRhZ3NlcnZpY2VzLmNvbTo0NDMiLCJmZWF0dXJlIjoiUHJpdmFjeVNhbmRib3hBZHNBUElzIiwiZXhwaXJ5IjoxNjYxMjk5MTk5LCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ=="><link as="script" rel="prefetch" href="/_next/static/chunks/pages/mypage-141f665c92390596.js"><link as="script" rel="prefetch" href="/_next/static/chunks/733-e7d84bed7ed356fa.js"><link as="script" rel="prefetch" href="/_next/static/chunks/pages/bookmark-61fff4ba51ad8155.js"><link as="script" rel="prefetch" href="/_next/static/chunks/866-c05af8e23c872e2c.js"><link as="script" rel="prefetch" href="/_next/static/chunks/pages/signin-ae60a19128cbc932.js"><link as="script" rel="prefetch" href="/_next/static/chunks/932-f01e4d1c2aeb2e91.js"><link as="script" rel="prefetch" href="/_next/static/chunks/pages/area-bf69ae687aa602d8.js"><link as="script" rel="prefetch" href="/_next/static/chunks/pages/tosalons-79196977b12f1828.js"><link as="script" rel="prefetch" href="/_next/static/chunks/pages/salon/%5B%5B...salon%5D%5D-15ea0d621d053ed1.js"><link rel="preload" href="https://adservice.google.co.jp/adsid/integrator.js?domain=kanunu-beauty.com" as="script"><script type="text/javascript" src="https://adservice.google.co.jp/adsid/integrator.js?domain=kanunu-beauty.com"></script><link rel="preload" href="https://adservice.google.com/adsid/integrator.js?domain=kanunu-beauty.com" as="script"><script type="text/javascript" src="https://adservice.google.com/adsid/integrator.js?domain=kanunu-beauty.com"></script><script src="https://apis.google.com/js/api.js?onload=__iframefcb771037" type="text/javascript" charset="UTF-8" gapi_processed="true"></script><link as="script" rel="prefetch" href="/_next/static/chunks/pages/privacy-policy-244018d019bd23ac.js"><link as="script" rel="prefetch" href="/_next/static/chunks/pages/terms-of-service-6bb94404533a0249.js">
</head>
_app.js
import React, { useEffect } from 'react'
import { useRouter } from 'next/router'
import Script from 'next/script'
import Head from 'next/head'
import * as gtag from '../lib/gtag.js'
import { GA_TRACKING_ID } from '../lib/gtag'
import { Provider } from "react-redux"
import redux from '../components/store/redux.js'
import '../../public/styles/Common.scss'
import '../../public/styles/Top.scss'
import '../../public/styles/index.scss'
import '../../public/styles/Header.scss'
import '../../public/styles/Footer.scss'
import '../../public/styles/ToSalons.scss'
import '../../public/styles/Terms.scss'
import '../../public/styles/Area.scss'
import '../../public/styles/Salon.scss'
import '../../public/styles/Search.scss'
import '../../public/styles/Bookmark.scss'
import '../../public/styles/SignUp.scss'
import '../../public/styles/SignUp.scss'
import '../../public/styles/Mypage.scss'
import '../../public/styles/SignUp.scss'
import '../../public/styles/SignUp.scss'
import "../../public/styles/Pagination.scss"
import Header from '../components/block/Header.js'
import Footer from '../components/block/Footer.js'
import { DefaultSeo } from 'next-seo'
import SEO from '../../next-seo.config'
import { config } from '@fortawesome/fontawesome-svg-core'
import '@fortawesome/fontawesome-svg-core/styles.css'
config.autoAddCss = false
export default function App({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
const handleComplete = (url) => {
// google analyticsの設定
gtag.pageview(url)
};
router.events.on("routeChangeComplete", handleComplete);
router.events.on("routeChangeError", handleComplete);
return () => {
router.events.off('routeChangeComplete', handleComplete)
}
},[router])
return (
<>
<Head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
<link rel="icon" href="https://test.com/favicon.ico" />
<link rel="apple-touch-icon" href="https://test.com/favicon.ico" />
</Head>
<DefaultSeo {...SEO} />
{/* Analytics */}
<Script
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
/>
<Script
id="analytics"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_TRACKING_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
<Provider store={redux}>
<Header />
<Component {...pageProps} />
<Footer />
</Provider>
</>
)
}
_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html lang="ja">
<Head>
<meta charSet="UTF-8" />
<meta name="theme-color" content="#ff889e" />
{/* google広告 */}
<script async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=xxxxxxxxxxx"
crossOrigin="anonymous"></script>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|