In short
Search Console reported 'Crawled - currently not indexed' with no referring sitemaps and no referring page, while robots.txt, canonicals and the sitemap were all verifiably correct.
The portfolio grids fetched projects client-side, so the server HTML carried only 173 words. Seeding the component's state from build-time data on the server page put every card in the HTML response and took the page from 173 words to 277.
Search Console gave me this on a page I was fairly sure was fine:
Page indexing
Page is not indexed: Crawled - currently not indexed
Discovery
Sitemaps: No referring sitemaps detected
Referring page: None detected
Crawl
Crawl allowed? Yes
Page fetch: Successful
Indexing
User-declared canonical: N/A
Google-selected canonical: N/ACrawl allowed, fetch successful, and then a refusal. That combination is worth understanding, because it rules out most of what people check first.
What it is not#
Crawl allowed: Yes and Page fetch: Successful mean robots.txt is not blocking anything and the server returned the page. I confirmed both anyway:
curl -s https://kartik.contentcalendar.in/robots.txt
# User-Agent: *
# Allow: /
# Disallow: /admin
# Sitemap: https://kartik.contentcalendar.in/sitemap.xmlThe sitemap was live and valid. Canonicals were present and self-referencing on every page. None of it mattered, because "Crawled - currently not indexed" is not an error. It is a judgement. Google fetched the page, looked at it, and decided it was not worth an index slot.
Measuring instead of guessing#
The useful question is what Googlebot actually received. Not what the browser shows — the browser shows you the page after JavaScript has run, which is precisely the view that hides this problem.
So: fetch the raw HTML, strip scripts and styles, count what is left.
import re, urllib.request
for path in ['/', '/web', '/ai', '/editing']:
html = urllib.request.urlopen('https://example.com' + path).read().decode('utf-8', 'ignore')
body = re.sub(r'(?is)<script.*?</script>', ' ', html)
body = re.sub(r'(?is)<style.*?</style>', ' ', body)
body = re.sub(r'(?s)<[^>]+>', ' ', body)
body = re.sub(r'\s+', ' ', body).strip()
print(f'{path:<10} {len(body.split()):>5} words')The result:
| Page | Words in server HTML |
|---|---|
/ | 1,601 |
/ai | 264 |
/editing | 174 |
/web | 173 |
There it is. The homepage was fine. Every portfolio page was serving under 300 words — and those pages are supposed to be the ones showing the work.
Why#
The grids looked like this:
const WebPortfolio = () => {
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchProjects = async () => {
const data = await getProjects(); // Firestore, in the browser
setProjects(data.filter((p) => p.section === 'web'));
setLoading(false);
};
fetchProjects();
}, []);
// ...
};Standard client-side fetching, and the component works perfectly. But the initial state is [], so the server renders zero cards. Every project title, category and client name — the entire substance of the page — appeared only after JavaScript executed and a network round-trip to Firestore completed.
Google does render JavaScript. But rendering is a second pass on Google's own schedule, and a page has to look worth the resources on the first pass to earn it. A near-empty page can be judged before its content ever exists.
The fix#
Nothing exotic: give the component its data on the server, and let the client fetch refresh it.
// src/app/web/page.jsx - a server component
import seedProjects from '../../data/portfolio.json';
export default function WebPortfolioPage() {
return <WebPortfolio initialProjects={seedProjects.filter((p) => p.section === 'web')} />;
}// the grid - two lines changed
const WebPortfolio = ({ initialProjects = [] }) => {
const [projects, setProjects] = useState(initialProjects);
const [loading, setLoading] = useState(initialProjects.length === 0);
// the existing useEffect still runs and replaces this with live dataThe useEffect is untouched. Firestore still owns the live data. The only change is that the first paint — and therefore the HTML response — is no longer empty.
| Page | Before | After |
|---|---|---|
/web | 173 words, 0 cards | 277 words, 6 cards |
| A blog post | n/a | ~1,300 words |
Verified by counting project titles in the raw HTML rather than trusting the word count alone:
web = [p for p in portfolio if p['section'] == 'web']
html = urllib.request.urlopen('http://localhost:3000/web').read().decode()
for record in web:
print('IN HTML' if record['title'] in html else 'MISSING', record['title'])Six of six.
The other half: the sitemap was never submitted#
No referring sitemaps detected had me checking the sitemap repeatedly. It was fine every time.
That field does not report whether a valid sitemap exists. It reports whether Google found this URL through a sitemap submitted in Search Console. Mine had never been submitted. A sitemap at a conventional path is discoverable in principle, but submitting it explicitly is what creates the association that field is describing.
Search Console → Sitemaps → sitemap.xml → Submit. That is the entire fix, and no amount of work in the repository substitutes for it.
What this demonstrates#
The diagnostic mistake I nearly made was treating "not indexed" as a configuration bug. Configuration bugs are satisfying to hunt: there is a file, it has a wrong line, you fix the line. So I checked robots.txt, then canonicals, then the sitemap, then trailing-slash redirects — all correct, all irrelevant.
"Crawled - currently not indexed" is Google declining to spend an index slot. The question it is answering is whether the page is worth indexing, and the only way to argue with that is to make the page carry something.
Two habits came out of this that I would keep:
Measure the response, not the page. curl and count words. The browser renders your JavaScript for you, which is exactly the help you do not want when diagnosing what a crawler received.
Read Search Console's fields literally. "No referring sitemaps" means no submitted sitemap referred this URL. It does not mean the sitemap is broken. Every minute I spent re-validating the XML was a minute spent on a field I had misread.
- seo
- nextjs
- indexing
- search-console
- rendering
Common questions
What does 'Crawled - currently not indexed' actually mean?
Google fetched the page successfully and then decided not to add it to the index. It is a quality and value judgement, not an error. Robots.txt, canonical tags and the sitemap can all be perfect and the page can still be refused, which is why checking those first often wastes time. The most common underlying cause on a JavaScript site is that the server response contains very little content.
Does Google not render JavaScript?
It does, but rendering is a second pass that happens on Google's own schedule, and a page has to earn that pass by looking worth the resources on the first one. If the initial HTML is nearly empty, the page can be judged before its content ever exists. Putting the content in the server response removes the dependency entirely.
Why did Search Console say 'No referring sitemaps detected' when the sitemap works?
That field reports whether the URL was found through a sitemap submitted in Search Console, not whether a valid sitemap exists on the site. A sitemap can be live, valid and reachable and still show this, because it was never submitted under Sitemaps in the Search Console property.
How do I check how much content my page actually serves?
Fetch the URL with curl or any HTTP client rather than opening it in a browser, then strip the script and style blocks and count the remaining words. The browser shows you the page after JavaScript has run, which is the view that hides this class of problem. View Source, not Inspect Element.
Share this
Instagram has no web share link, so this gives you both pieces: copy the caption, save the card, then post it.
Got this problem too?
Bespoke React and Next.js platforms, SaaS interfaces, client portals and interactive tools built for speed.