Common JavaScript SEO Issues
Framework-specific problems and practical solutions for React, Vue, Angular, Next.js, Nuxt, Svelte, and Astro.
Universal Issues (All Frameworks)
1. Client-Only Meta Tags
Problem: Setting <title>, <meta>, and <link> tags with JavaScript after page load.
Impact: Crawlers that don't execute JavaScript see no metadata. Your page may not rank or appear correctly in search results.
Solution: Use server-side rendering (SSR) or static site generation (SSG) to inject metadata in the initial HTML. For SPAs, use libraries like react-helmet, vue-meta, or @angular/platform-browser with SSR.
2. Content in JavaScript-Only Routes
Problem: Single-page apps render content client-side. The raw HTML is an empty shell.
<!-- Raw HTML: empty body -->
<div id="app"></div>
<!-- After JS: full content -->
<div id="app">
<h1>Welcome to My Site</h1>
<p>Content here...</p>
</div>
Impact: Crawlers see no content. Page won't rank.
Solution: Adopt SSR or pre-rendering for public-facing pages. Tools like Next.js, Nuxt, SvelteKit, and Astro make this straightforward.
3. Lazy-Loaded Images Without Fallbacks
Problem: Images loaded via JavaScript after scroll or interaction. No <img> tags in raw HTML.
Impact: Image search misses your images. Alt text isn't indexed.
Solution: Use <img loading="lazy"> with proper src and alt in the HTML. The browser handles lazy loading natively.
React-Specific Issues
Client-Side Routing (React Router)
Problem: React apps often use client-side routing. The server returns the same HTML shell for all routes.
Solution:
- Use Next.js for SSR/SSG with built-in routing
- Use Remix for server-side rendering with nested routes
- Implement SSR with React Router v6+ and
remix-run/router
useEffect for Data Fetching
Problem: Fetching data in useEffect means content loads after initial render.
function ProductPage() {
const [product, setProduct] = useState(null);
useEffect(() => {
fetch('/api/product').then(res => res.json()).then(setProduct);
}, []);
return <div>{product ? product.name : 'Loading...'}</div>;
}
Impact: Raw HTML shows "Loading..." — no product data.
Solution: Use Next.js getServerSideProps or getStaticProps to fetch data server-side.
export async function getServerSideProps() {
const product = await fetchProduct();
return { props: { product } };
}
function ProductPage({ product }) {
return <div>{product.name}</div>;
}
Vue-Specific Issues
Vue Router + Client-Only Rendering
Problem: Vue apps with Vue Router render client-side by default.
Solution: Use Nuxt for SSR/SSG with automatic routing and SEO optimization.
Async Components
Problem: Components loaded asynchronously may not render in the initial HTML.
Solution: With Nuxt, use asyncData or fetch hooks to load data server-side.
<script>
export default {
async asyncData({ params }) {
const product = await fetch(`/api/products/${params.id}`).then(r => r.json());
return { product };
}
}
</script>
Angular-Specific Issues
Delayed ngOnInit Rendering
Problem: Data fetched in ngOnInit isn't present in the initial HTML.
Solution: Use Angular Universal for server-side rendering. Fetch data in resolvers before route activation.
@Injectable()
export class ProductResolver implements Resolve<Product> {
resolve(route: ActivatedRouteSnapshot): Observable<Product> {
return this.productService.getProduct(route.params.id);
}
}
Next.js Best Practices
Next.js is built for SEO, but you can still make mistakes:
Using Client Components for Everything
Problem: Marking all components with 'use client' disables server rendering.
Solution: Use Server Components by default. Only add 'use client' for interactive components.
Dynamic Imports Without SSR
Problem: next/dynamic with { ssr: false } skips server rendering.
Solution: Avoid { ssr: false } unless absolutely necessary (e.g., browser-only libraries).
Nuxt Best Practices
Client-Only Plugins
Problem: Plugins that only run on the client can delay content rendering.
Solution: Use mode: 'client' sparingly. Ensure critical content renders server-side.
Svelte and SvelteKit
Svelte (Without SvelteKit)
Problem: Plain Svelte apps are client-rendered SPAs.
Solution: Use SvelteKit for SSR and SSG.
SvelteKit Load Functions
Use load functions in +page.server.js to fetch data server-side.
export async function load({ params }) {
const product = await fetch(`/api/products/${params.id}`).then(r => r.json());
return { product };
}
Astro
Astro is designed for content-first sites with minimal JavaScript. It's excellent for SEO out of the box.
Avoiding Client-Side Hydration Everywhere
Problem: Using client:load or client:visible on all components adds unnecessary JavaScript.
Solution: Keep components static by default. Only hydrate interactive islands.
---
import Counter from './Counter.jsx';
---
<Counter client:visible />
General Recommendations
- Prefer SSR/SSG over CSR for public-facing content
- Test with JavaScript disabled in DevTools to see what crawlers see
- Use WatchThis regularly during development to catch regressions
- Monitor Core Web Vitals — slow JS execution hurts SEO and UX
- Progressive enhancement: Start with working HTML, enhance with JS