-
SummaryI'm using Headless WP hosted on rocket.net as backend and Rankmath for generating metadata, the issue is that when I fetch the data in a dynamic page it renders in head the first time, but after I refresh the page it moves to body outside head, Here is the complete code of the page where this issue is happening: import BlogDetailHero from "@/components/BlogDetails/BlogDetailHero";
import RecentPosts from "@/components/BlogDetails/RecentPosts";
import RelatedArticles from "@/components/BlogDetails/RelatedArticles";
import Understanding from "@/components/BlogDetails/Understanding";
import TalkProperty from "@/components/Shared/TalkProperty/TalkProperty";
import React from "react";
import { getPostBySlug } from "@/lib/getPostBySlug";
import { SinglePost } from "@/types/singlePost";
import { JSDOM } from "jsdom";
import { Metadata } from "next";
import TOC from "@/components/BlogDetails/TOC";
import { formatDate, formatTime } from "@/utilities/UtilitiesFunctions";
import { getPostsByCategoryId } from "@/lib/getPostsByCategories";
import { Post } from "@/types/post";
import PopularQuestions from "@/components/Shared/PopularQuestions/PopularQuestions";
import { getRecentPosts } from "@/lib/getRecentPosts";
import ClientOnly from "@/components/BlogDetails/ClientOnly";
interface PageProps {
params: Promise<{
slug: string;
}>;
}
export async function generateMetadata(props: PageProps): Promise<Metadata> {
const { slug } = await props.params;
const siteUrl = process.env.NEXT_PUBLIC_WORDPRESS_SITE_URL!;
const requestUrl = `${siteUrl}/wp-json/rankmath/v1/getHead?url=${siteUrl}/${slug}`;
const res = await fetch(requestUrl, { next: { revalidate: 3600 } });
if (!res.ok) return {};
const json = await res.json();
const head = json.head as string;
const dom = new JSDOM(head);
const document = dom.window.document;
// Safe getter helper (prevents TS null issues)
const getMeta = (selector: string): string | undefined => {
const el = document.querySelector(selector) as HTMLMetaElement | null;
return el?.getAttribute("content") || undefined;
};
const getHref = (selector: string): string | undefined => {
const el = document.querySelector(selector) as HTMLLinkElement | null;
return el?.getAttribute("href") || undefined;
};
// Extract fields with safety
const title =
getMeta('meta[property="og:title"]') ||
getMeta('meta[name="twitter:title"]') ||
undefined;
const description =
getMeta("meta[name='description']") ||
getMeta('meta[property="og:description"]') ||
undefined;
// Canonical fix (TS safe)
let canonicalRaw = getHref("link[rel='canonical']");
let canonical: string | undefined;
if (canonicalRaw && canonicalRaw.startsWith("http")) {
canonical = canonicalRaw
.replace("https://cms.nourestates.com", "https://www.nourestates.com")
.replace(/\/+$/, ""); // remove trailing slashes
}
const ogImage = getMeta('meta[property="og:image"]');
const twitterImage = getMeta('meta[name="twitter:image"]');
// Return metadata (TS typed)
return {
title,
description,
alternates: canonical ? { canonical } : undefined,
openGraph: {
title: title,
description: description,
images: ogImage ? [{ url: ogImage }] : undefined,
},
twitter: {
title,
description,
images: twitterImage ? [{ url: twitterImage }] : undefined,
},
};
}
const page = async ({ params }: { params: Promise<{ slug: string }> }) => {
const { slug } = await params;
const post: SinglePost | null = await getPostBySlug(slug);
const recentPosts = await getRecentPosts(post?.databaseId);
if (!post) {
return <p>Post not found</p>;
}
function stripRankMathHead(content: string): string {
if (!content) return content;
const dom = new JSDOM(content);
const doc = dom.window.document;
// Remove RankMath injected meta tags
doc
.querySelectorAll(
'meta[content*="rank-math"], meta[property*="og:"], meta[name*="twitter:"], meta[name="description"]'
)
.forEach((el) => el.remove());
// Remove canonical links
doc.querySelectorAll('link[rel="canonical"]').forEach((el) => el.remove());
// Remove JSON-LD schema blocks
doc
.querySelectorAll('script[type="application/ld+json"]')
.forEach((el) => el.remove());
// Remove any RankMath comments
doc.querySelectorAll("*").forEach((el) => {
el.childNodes.forEach((node) => {
if (node.nodeType === 8 && node.nodeValue?.includes("rank")) {
node.remove();
}
});
});
return doc.body.innerHTML;
}
function cleanFaqTextServer(raw: string): string {
if (!raw) return "";
let s = String(raw)
.replace(/\\u003c/gi, "<")
.replace(/\\u003e/gi, ">")
.replace(/u003c/gi, "<")
.replace(/u003e/gi, ">")
.replace(/ /gi, " ")
.replace(/&/gi, "&");
s = s.replace(/&#x([0-9a-f]+);/gi, (_, hex) =>
String.fromCharCode(parseInt(hex, 16))
);
s = s.replace(/&#([0-9]+);/g, (_, dec) =>
String.fromCharCode(parseInt(dec, 10))
);
const dom = new JSDOM(s);
return (dom.window.document.body.textContent || "").trim();
}
function extractFAQsFromContent(content: string) {
const dom = new JSDOM(content);
const doc = dom.window.document;
const faqElements = Array.from(
doc.querySelectorAll(".rank-math-list > div")
);
return faqElements.map((faq) => {
const rawQuestion =
faq.querySelector(".rank-math-question")?.textContent?.trim() || "";
const rawAnswer =
faq.querySelector(".rank-math-answer")?.textContent?.trim() || "";
return {
question: cleanFaqTextServer(rawQuestion),
answer: cleanFaqTextServer(rawAnswer),
};
});
}
function stripRankMathFAQ(content: string): string {
const dom = new JSDOM(content);
const doc = dom.window.document;
const faqBlock = doc.querySelector(".rank-math-list");
if (faqBlock) faqBlock.remove();
doc.querySelectorAll("h2").forEach((h2) => {
if (h2.textContent?.toLowerCase().includes("faq")) {
h2.remove();
}
});
return doc.body.innerHTML;
}
const faqsRaw = extractFAQsFromContent(post?.content ?? "");
const faqs = faqsRaw.map((faq) => ({
heading: faq.question,
text1: faq.answer,
}));
const categoryIds = post.categories.nodes.map((c) => Number(c.databaseId));
async function getRelatedPosts(postCategories: number[], limit = 3) {
let selectedCats: number[] = [];
if (postCategories.length === 1) {
selectedCats = [postCategories[0]];
} else if (postCategories.length === 2) {
selectedCats = postCategories;
} else if (postCategories.length >= 3) {
selectedCats = postCategories.slice(0, 2);
}
return await getPostsByCategoryId(selectedCats, limit);
}
const relatedPosts = await getRelatedPosts(categoryIds, 3);
return (
<main className="w-full">
<ClientOnly>
<BlogDetailHero
title={post.title}
date={formatDate(post.date)}
time={formatTime(post.date)}
featImgUrl={post.featuredImage?.node?.sourceUrl || ""}
altText={post.featuredImage?.node?.altText || ""}
/>
</ClientOnly>
<ClientOnly>
<aside className="block md:hidden px-6 w-full">
<TOC content={post.content} />
</aside>
</ClientOnly>
<ClientOnly>
<section className="blog-details">
<div className="relative w-full flex flex-col lg:flex-row lg:justify-between lg:gap-10 px-6 md:px-15 lg:px-20">
<article className="w-full lg:w-4xl">
<Understanding
content={stripRankMathHead(stripRankMathFAQ(post.content))}
/>
</article>
<hr className="border-t border-gray-300 w-[350px] ml-5 md:hidden" />
<aside className="w-full hidden xl:block xl:w-sm">
<TOC content={post.content} />
<RecentPosts posts={recentPosts} />
</aside>
</div>
</section>
</ClientOnly>
{faqs.length > 0 && (
<ClientOnly>
<PopularQuestions
heading="Frequently Asked Questions"
Questions={faqs}
/>
</ClientOnly>
)}
<ClientOnly>
<RelatedArticles posts={relatedPosts} />
<TalkProperty />
</ClientOnly>
</main>
);
};
export default page;Additional informationNo response Examplehttps://www.nourestates.com/investing-in-lombok-real-estates |
Beta Was this translation helpful? Give feedback.
Answered by
icyJoseph
Nov 27, 2025
Replies: 1 comment 2 replies
-
|
Try with this https://nextjs.org/docs/app/api-reference/functions/generate-metadata#streaming-metadata |
Beta Was this translation helpful? Give feedback.
2 replies
Answer selected by
Amanat838
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Try with this https://nextjs.org/docs/app/api-reference/functions/generate-metadata#streaming-metadata