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

Further Reading