← Zurück zur Übersicht
Editorial-Bild: Next.js SEO: Technische Optimierung die rankt

Das Wichtigste zuerst

Next.js bringt SSR, ISR und Metadata-API von Haus aus mit, aber ohne bewusste SEO-Konfiguration verschenkst du das Potenzial und bleibst für Google teilweise unsichtbar.

Warum Next.js für SEO besser ist als reines React

React hat ein fundamentales SEO-Problem: Es rendert standardmäßig im Browser (Client-Side Rendering, CSR). Das bedeutet: Wenn Google deine Seite crawlt, sieht es zunächst ein leeres

, und muss den JavaScript-Code erst ausführen, um den Inhalt zu sehen.

Google kann JavaScript rendern, aber mit Einschränkungen:

Merksatz: „Googlebot kann JavaScript. Aber er ist langsam, ungeduldig und rendert in einer Warteschlange. Was er sofort lesen kann, indexiert er sofort."

Next.js löst dieses Problem durch Server-Side Rendering (SSR) und Static Site Generation (SSG): Der HTML-Code wird bereits auf dem Server oder zur Build-Zeit generiert und als vollständiges HTML an den Browser (und an Googlebot) ausgeliefert. Kein Warten auf JavaScript-Execution.

| Aspekt | Reines React (CSR) | Next.js (SSR/SSG) |

|--------|-------------------|-------------------|

| Erster HTML-Response | Leeres

+ JS-Bundle | Vollständiges HTML |

| Googlebot-Indexierung | Verzögert (JavaScript Rendering Queue) | Sofort |

| Core Web Vitals (LCP) | Schlecht (JS muss erst laden) | Gut (HTML direkt da) |

| Social Media Previews | Oft leer (kein SSR) | Korrekt (Meta-Tags im HTML) |

| Time to First Byte | Schnell (nur statische Datei) | Schnell (SSG) / Mittel (SSR) |

Rendering-Strategien und ihre SEO-Auswirkungen

Next.js bietet vier Rendering-Strategien. Jede hat unterschiedliche SEO-Implikationen:

Static Site Generation (SSG), der SEO-Champion

Seiten werden zur Build-Zeit generiert. Das Ergebnis ist pures HTML, das vom CDN ausgeliefert wird. Perfekt für Content, der sich selten ändert.

PROT_3

SEO-Vorteil: Schnellste Ladezeit (direkt vom CDN), beste Core Web Vitals, sofort crawlbar.

Nachteil: Bei Inhaltsänderungen muss neu gebaut werden, oder ISR nutzen.

Server-Side Rendering (SSR), für dynamische Inhalte

Seiten werden bei jedem Request auf dem Server generiert. Sinnvoll für personalisierte oder häufig wechselnde Inhalte.

PROT_4

SEO-Vorteil: Immer aktueller Inhalt, vollständiges HTML für Crawlers.

Nachteil: Höhere Server-Last, langsamere Time to First Byte als SSG.

Incremental Static Regeneration (ISR), das Beste aus beiden Welten

Seiten werden statisch generiert, aber in definierten Intervallen im Hintergrund aktualisiert. Der Nutzer sieht die gecachte Version, während der Server die neue Version baut.

PROT_5

SEO-Vorteil: CDN-Geschwindigkeit + aktuelle Inhalte. Perfekt für Blogs, Produktseiten, Landingpages.

Client-Side Rendering (CSR), der SEO-Killer

Inhalte werden erst im Browser gerendert. Für SEO-relevante Seiten nicht verwenden.

Wann CSR OK ist: Dashboard-Bereiche hinter Login, interaktive Tools, User-spezifische Inhalte die nicht indexiert werden sollen.

Merksatz: „SSG für alles was sich selten ändert. ISR für alles was sich regelmäßig ändert. SSR für alles was sich bei jedem Request ändert. CSR für alles was Google nie sehen soll."

Rendering-ZeitpunktBuild-Zeit
SEO-TauglichkeitExzellent
Core Web VitalsBeste Werte
AktualitätNur nach Rebuild
Ideal fürBlogs, Landingpages, Docs

Kernerkenntnis

Next.js löst das Crawling-Problem aus der Box, aber das ist nicht das gleiche wie das Ranking-Problem zu lösen. Die Defaults sind SEO-fähig, nicht SEO-fertig. Den Unterschied macht, was du danach konfigurierst.

Meta-Tags und Open Graph in Next.js richtig setzen

Die Metadata API (Next.js 14+ App Router)

Next.js hat mit dem App Router eine elegante Metadata API eingeführt. Statische und dynamische Meta-Tags lassen sich direkt in der Page-Komponente definieren:

Statische Metadata:

PROT_6

Dynamische Metadata:

PROT_7

Wichtig: Jede Seite braucht einen einzigartigen title und eine einzigartige description. Die Metadata API sorgt automatisch dafür, dass diese ins des HTML gerendert werden, ohne manuelles -Management.

Title-Tag Best Practices

  • Maximal 60 Zeichen (Google kürzt sonst ab)
  • Fokus-Keyword möglichst am Anfang
  • Brand-Name am Ende mit Separator: Titel | Brand
  • Jede Seite ein eigener Title, kein Duplicate

Meta-Description Best Practices

  • 150–160 Zeichen
  • Enthält das Fokus-Keyword (wird von Google gefettet)
  • Call-to-Action oder Nutzenversprechen
  • Kein Duplicate über mehrere Seiten

Technisches SEO in Next.js: Sitemap, Robots, Canonical

XML-Sitemap automatisch generieren

Next.js unterstützt seit Version 13.3 eine native Sitemap-Generierung:

PROT_13

Die Sitemap wird automatisch unter /sitemap.xml ausgeliefert. Bei großen Seiten mit 50.000+ URLs kannst du generateSitemaps() nutzen, um mehrere Sitemaps zu erzeugen.

robots.txt konfigurieren

PROT_16

Canonical URLs setzen

Canonical URLs sind essenziell, um Duplicate Content zu vermeiden, besonders bei Seiten mit URL-Parametern (Filter, Sortierung, Paginierung).

PROT_17

Regel: Jede indexierbare Seite braucht eine Canonical URL. Die Canonical zeigt immer auf die „Hauptversion" der Seite, ohne Tracking-Parameter, ohne Session-IDs.

Structured Data (JSON-LD) in Next.js einbinden

Structured Data hilft Google, den Inhalt deiner Seite zu verstehen, und ermöglicht Rich Snippets in den Suchergebnissen (FAQ-Dropdowns, How-To-Schritte, Bewertungssterne, Breadcrumbs).

PROT_18

Die wichtigsten Schema-Typen für Content-Websites:

| Schema-Typ | Wofür | Rich-Snippet-Effekt |

|------------|-------|---------------------|

| Article | Blog-Posts, Artikel | Datum, Autor in den SERPs |

| FAQPage | FAQ-Sektionen | Expandierbare Fragen in Google |

| HowTo | Anleitungen, Tutorials | Schritte als Liste in Google |

| BreadcrumbList | Navigation / Breadcrumbs | Breadcrumb-Pfad in den SERPs |

| LocalBusiness | Lokale Unternehmen | Knowledge Panel, Maps |

| Organization | Unternehmensdaten | Logo, Social-Links im Panel |

Merksatz: „Structured Data ist kein Ranking-Faktor. Aber es ist ein CTR-Faktor, und CTR beeinflusst Rankings indirekt."

Core Web Vitals in Next.js optimieren

LCP (Largest Contentful Paint), das Hauptbild schneller laden

LCP misst, wie schnell das größte sichtbare Element geladen wird. Ziel: unter 2,5 Sekunden.

Next.js Hebel:

  • next/image verwenden statt : Automatische Lazy-Loading, WebP/AVIF-Konvertierung, responsive Sizes
  • priority-Prop für Above-the-Fold-Bilder setzen (deaktiviert Lazy Loading)
  • next/font für Schriften: Eliminiert Layout Shift durch Font-Loading
PROT_23

CLS (Cumulative Layout Shift), Sprünge vermeiden

CLS misst visuelle Stabilität. Ziel: unter 0,1.

Next.js Hebel:

  • width und height bei allen Images definieren (reserviert Platz)
  • next/font mit display: swap und adjustFontFallback
  • Keine dynamischen Inhalte über dem Fold einblenden (Ads, Banner)
PROT_29

INP (Interaction to Next Paint), Interaktionen schnell machen

INP misst die Reaktionszeit auf Nutzer-Interaktionen. Ziel: unter 200ms.

Next.js Hebel:

  • Schwere Client-Komponenten mit React.lazy() und Suspense splitten
  • 'use client' nur dort wo nötig, alles andere als Server Component belassen
  • Third-Party Scripts mit next/script und strategy="lazyOnload" laden
PROT_35

Die 10 häufigsten Next.js SEO-Fehler

| # | Fehler | Lösung |

|---|--------|--------|

| 1 | Keine Metadata API nutzen, leerer </code> | <code>generateMetadata</code> für jede Route |</p> <p>| 2 | Alle Seiten als CSR (Client Components) | Server Components als Default |</p> <p>| 3 | Keine Sitemap | <code>app/sitemap.ts</code> anlegen |</p> <p>| 4 | Fehlende Canonical URLs | <code>alternates.canonical</code> in Metadata |</p> <p>| 5 | <code><img></code> statt <code>next/image</code> | Image-Component mit <code>priority</code> für ATF |</p> <p>| 6 | Custom Fonts ohne <code>next/font</code> | <code>next/font</code> für Zero-CLS-Fonts |</p> <p>| 7 | robots.txt vergessen | <code>app/robots.ts</code> anlegen |</p> <p>| 8 | JSON-LD fehlt | <code><script type="application/ld+json"></code> einbauen |</p> <p>| 9 | Third-Party Scripts blockieren Rendering | <code>next/script</code> mit <code>lazyOnload</code> |</p> <p>| 10 | Kein <code>alt</code>-Text bei Bildern | Beschreibendes <code>alt</code> für jedes Image |</p> <section class="faq-section reveal" role="region" aria-label="Häufige Fragen zu Next.js SEO"> <p class="faq-label">FAQ</p> <h2 class="faq-title">Häufige Fragen</h2> <div class="faq-item" id="faq-1"> <button class="faq-q" aria-expanded="false" aria-controls="faq-1-a" id="faq-1-btn">Kann Google JavaScript-Seiten indexieren?<span class="faq-chevron" aria-hidden="true">⌄</span></button> <div class="faq-a" id="faq-1-a" role="region" aria-labelledby="faq-1-btn"><div class="faq-a-inner">Ja, aber mit Einschränkungen. Googlebot nutzt eine relativ aktuelle Chrome-Version zum Rendern, aber JavaScript-Rendering läuft in einer Warteschlange und kann Tage dauern. SSR/SSG-Seiten werden sofort indexiert, CSR-Seiten mit Verzögerung. Für SEO-relevante Inhalte immer SSR oder SSG verwenden.</div></div> </div> <div class="faq-item" id="faq-2"> <button class="faq-q" aria-expanded="false" aria-controls="faq-2-a" id="faq-2-btn">Was ist besser für SEO: SSG oder SSR?<span class="faq-chevron" aria-hidden="true">⌄</span></button> <div class="faq-a" id="faq-2-a" role="region" aria-labelledby="faq-2-btn"><div class="faq-a-inner">SSG ist in den meisten Fällen besser: schnellere Ladezeiten, bessere Core Web Vitals, CDN-Caching. SSR ist nur dann sinnvoll, wenn sich Inhalte bei jedem Request ändern müssen (z. B. personalisierte Preise). Für Content-Websites und Blogs ist ISR (Incremental Static Regeneration) der Sweet Spot.</div></div> </div> <div class="faq-item" id="faq-3"> <button class="faq-q" aria-expanded="false" aria-controls="faq-3-a" id="faq-3-btn">Brauche ich ein SEO-Plugin für Next.js?<span class="faq-chevron" aria-hidden="true">⌄</span></button> <div class="faq-a" id="faq-3-a" role="region" aria-labelledby="faq-3-btn"><div class="faq-a-inner">Nein. Die eingebaute Metadata API, <code>app/sitemap.ts</code> und <code>app/robots.ts</code> decken die Grundlagen ab. Für JSON-LD kann das Package <code>next-seo</code> oder <code>schema-dts</code> hilfreich sein, ist aber nicht zwingend nötig. Die meisten „SEO-Plugins" für Next.js wrappen nur die eingebauten Features.</div></div> </div> <div class="faq-item" id="faq-4"> <button class="faq-q" aria-expanded="false" aria-controls="faq-4-a" id="faq-4-btn">Wie teste ich, ob meine Next.js-Seite SEO-ready ist?<span class="faq-chevron" aria-hidden="true">⌄</span></button> <div class="faq-a" id="faq-4-a" role="region" aria-labelledby="faq-4-btn"><div class="faq-a-inner">Vier Tools reichen: (1) Google Search Console, Indexierung und Crawl-Errors, (2) Google Rich Results Test, Structured Data validieren, (3) PageSpeed Insights, Core Web Vitals prüfen, (4) <code>curl -A Googlebot URL</code> im Terminal, prüfen ob HTML vollständig ausgeliefert wird.</div></div> </div> <div class="faq-item" id="faq-5"> <button class="faq-q" aria-expanded="false" aria-controls="faq-5-a" id="faq-5-btn">Funktioniert Next.js SEO auch mit dem Pages Router?<span class="faq-chevron" aria-hidden="true">⌄</span></button> <div class="faq-a" id="faq-5-a" role="region" aria-labelledby="faq-5-btn"><div class="faq-a-inner">Ja, aber die Metadata API ist nur im App Router verfügbar. Im Pages Router nutzt du <code>next/head</code> für Meta-Tags und <code>getStaticProps</code>/<code>getServerSideProps</code> für SSG/SSR. Für neue Projekte: App Router ist die Zukunft.</div></div> </div> </section> <div class="sources reveal"> <h2>Quellen & Vertiefung</h2> <ul><li>Erlhofer, S. (2024). <em>Suchmaschinen-Optimierung: Das umfassende Handbuch.</em> 11. Aufl., Rheinwerk Computing., Kap. 8.12.4 (Core Web Vitals), Kap. 10 (Onpage-Optimierung)</li> <li>Deges, F. (2023). <em>Grundlagen des E-Commerce.</em> 2. Aufl., Springer Gabler., Kap. 10.1.3 (Conversion Rate)</li> <li>Next.js Docs, Metadata API: <a href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata" target="_blank" rel="noopener">https://nextjs.org/docs/app/building-your-application/optimizing/metadata</a></li> <li>Next.js Docs, Sitemap: <a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap" target="_blank" rel="noopener">https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap</a></li> <li>Next.js Docs, Image Optimization: <a href="https://nextjs.org/docs/app/building-your-application/optimizing/images" target="_blank" rel="noopener">https://nextjs.org/docs/app/building-your-application/optimizing/images</a></li> <li>Google Search Central, JavaScript SEO: <a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics" target="_blank" rel="noopener">https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics</a></li> <li>Google, Core Web Vitals: <a href="https://web.dev/vitals/" target="_blank" rel="noopener">https://web.dev/vitals/</a></li> <li>Google, Rich Results Test: <a href="https://search.google.com/test/rich-results" target="_blank" rel="noopener">https://search.google.com/test/rich-results</a></li> <li>Vercel Blog, Next.js SEO: <a href="https://vercel.com/blog/nextjs-seo" target="_blank" rel="noopener">https://vercel.com/blog/nextjs-seo</a></li> <li>Schema.org, Structured Data: <a href="https://schema.org/" target="_blank" rel="noopener">https://schema.org/</a></li></ul> </div> </article> <a class="back-link back-link--end" href="/artikel">Zurück zur Übersicht</a> <!-- back-link-end-injected --> </main> <aside class="article-end-cta"> <span class="article-end-cta-label">// hat dir das geholfen?</span> <h3>Brauchst du Sparring für dein SEO?</h3> <p>Ich bin Max, SEO Strategist aus Bochum. Von Audit über Content-Strategie bis Local SEO. KMU im DACH-Raum, evidenzbasiert, ohne Hype-Versprechen.</p> <div class="article-end-cta-row"> <a class="primary" href="mailto:hello@while.chat?subject=Anfrage%20SEO">projekt anfragen →</a> <a href="https://wa.me/4915222623657" target="_blank" rel="noopener">WhatsApp</a> <a href="/blog">mehr Artikel</a> </div> </aside> <section class="related-articles reveal" aria-label="Weiterlesen"> <h3>Weiterlesen</h3> <div class="related-grid"> <a class="related-card" href="/posts/deployment-checkliste"><span class="related-title">Deployment-Checkliste: Von localhost zu Production</span><span class="related-desc">Deine Website ist lokal fertig, jetzt muss sie live. Die Deployment-Checkliste in 5 Kategorien: Performance, Security, SEO, Accessibility...</span></a> <a class="related-card" href="/posts/claude-code-skills"><span class="related-title">Claude Code Skills: Dein eigenes Ökosystem in 30 Minuten</span><span class="related-desc">Claude Code Skills sind wiederverwendbare Bausteine für deinen AI-Workflow. So erstellst du deinen ersten Skill in 30 Minuten, mit Praxis-B...</span></a> <a class="related-card" href="/posts/anti-pattern-vibe-coding"><span class="related-title">Anti-Pattern: Wie Vibe Coding Projekte killt (und wie nicht)</span><span class="related-desc">Vibe Coding ist mächtig, aber gefährlich wenn du diese 7 Anti-Pattern nicht kennst. Von Copy-Paste-Architektur bis Deploy-und-Vergessen. Mi...</span></a> </div> </section> <footer class="footer"> <div class="footer-brand">while.chat</div> <p>Verstehen. Anwenden. Wachsen. Erstellt in Bochum</p> <p class="footer-nap"><a href="https://www.google.com/maps/search/?api=1&query=Hauptstra%C3%9Fe+278+44892+Bochum" rel="noopener" aria-label="Adresse in Maps öffnen">Hauptstr. 278 · 44892 Bochum</a> · <a href="tel:+4915222623657" aria-label="Anrufen">+49 1522 2623657</a></p> <div class="footer-links"> <a href="/datenschutz">Datenschutz</a> <a href="/impressum">Impressum</a> </div> <div class="footer-social"> <a href="https://www.facebook.com/profile.php?id=61577505480981" target="_blank" rel="noopener noreferrer" aria-label="Facebook"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg></a> </div> </footer> <script>window.va=window.va||function(){(window.vaq=window.vaq||[]).push(arguments)};</script> <script defer src="/_vercel/insights/script.js"></script> <script defer src="/_vercel/speed-insights/script.js"></script> <script> document.addEventListener('DOMContentLoaded',function(){ // === Scroll Reveal === var obs=new IntersectionObserver(function(entries){entries.forEach(function(e){if(e.isIntersecting)e.target.classList.add('visible')})},{threshold:0.12}); document.querySelectorAll('.reveal').forEach(function(el){obs.observe(el)}); document.querySelectorAll('.reveal').forEach(function(el){var r=el.getBoundingClientRect();if(r.top<window.innerHeight&&r.bottom>0)el.classList.add('visible')}); // === Nav Scroll === var nav=document.getElementById('nav'); if(nav)window.addEventListener('scroll',function(){nav.classList.toggle('scrolled',window.scrollY>40)},{passive:true}); // === Progress Bar === var bar=document.getElementById('progress-bar'); if(bar)window.addEventListener('scroll',function(){var t=document.documentElement.scrollHeight-window.innerHeight;bar.style.width=(window.scrollY/t*100)+'%'},{passive:true}); // === Burger === var burger=document.getElementById('burger'),overlay=document.getElementById('nav-overlay'); if(burger&&overlay){burger.addEventListener('click',function(){var o=burger.classList.toggle('open');overlay.classList.toggle('open');burger.setAttribute('aria-expanded',o);overlay.setAttribute('aria-hidden',!o);document.body.style.overflow=o?'hidden':''});overlay.querySelectorAll('a').forEach(function(a){a.addEventListener('click',function(){burger.classList.remove('open');overlay.classList.remove('open');burger.setAttribute('aria-expanded','false');overlay.setAttribute('aria-hidden','true');document.body.style.overflow=''})})} // === FAQ Accordion === document.querySelectorAll('.faq-item').forEach(function(item){ var btn=item.querySelector('.faq-q'); if(!btn)return; btn.addEventListener('click',function(){ var isOpen=item.classList.contains('open'); document.querySelectorAll('.faq-item').forEach(function(o){o.classList.remove('open');var ob=o.querySelector('.faq-q');if(ob)ob.setAttribute('aria-expanded','false')}); if(!isOpen){item.classList.add('open');btn.setAttribute('aria-expanded','true')} }); btn.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();btn.click()}}); }); // === Rendering Strategy Tabs === var tabBtns=document.querySelectorAll('.tab-btn[role="tab"]'); tabBtns.forEach(function(btn){ btn.addEventListener('click',function(){ var targetId=btn.getAttribute('aria-controls'); tabBtns.forEach(function(b){b.setAttribute('aria-selected','false')}); document.querySelectorAll('.tab-panel').forEach(function(p){p.setAttribute('aria-hidden','true');p.style.display='none'}); btn.setAttribute('aria-selected','true'); var target=document.getElementById(targetId); if(target){target.setAttribute('aria-hidden','false');target.style.display='block'} }); btn.addEventListener('keydown',function(e){ var tabs=Array.from(tabBtns);var idx=tabs.indexOf(btn); if(e.key==='ArrowRight'){e.preventDefault();tabs[(idx+1)%tabs.length].focus();tabs[(idx+1)%tabs.length].click()} if(e.key==='ArrowLeft'){e.preventDefault();tabs[(idx-1+tabs.length)%tabs.length].focus();tabs[(idx-1+tabs.length)%tabs.length].click()} }); }); document.querySelectorAll('.tab-panel').forEach(function(p,i){p.style.display=i===0?'block':'none'}); // === Three.js Hero: Code Grid === var canvas=document.getElementById('three-hero'); if(canvas&&!window.matchMedia('(prefers-reduced-motion: reduce)').matches){ function getThemeColors(){ var dark=document.documentElement.getAttribute('data-theme')==='dark'||(!document.documentElement.getAttribute('data-theme')&&window.matchMedia('(prefers-color-scheme: dark)').matches); return{accent:dark?0x5B85D6:0x2D5FC2,crema:dark?0x5B85D6:0x5B85D6,bg:dark?0x25201A:0xFAF7F0,line:dark?0x334155:0xD6CDBE}; } var isMobile=window.innerWidth<768; var scene=new THREE.Scene(); var c=getThemeColors(); scene.background=new THREE.Color(c.bg); var cam=new THREE.PerspectiveCamera(60,canvas.clientWidth/canvas.clientHeight,0.1,100); cam.position.set(0,0,14); var renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:!isMobile,alpha:false}); renderer.setSize(canvas.clientWidth,canvas.clientHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio,isMobile?1.5:2)); // Particles var pCount=isMobile?80:160; var pGeo=new THREE.BufferGeometry(); var pPos=new Float32Array(pCount*3); var pVel=[]; for(var i=0;i<pCount;i++){ pPos[i*3]=(Math.random()-0.5)*20; pPos[i*3+1]=(Math.random()-0.5)*12; pPos[i*3+2]=(Math.random()-0.5)*10; pVel.push((Math.random()-0.5)*0.008,(Math.random()-0.5)*0.008,(Math.random()-0.5)*0.005); } pGeo.setAttribute('position',new THREE.BufferAttribute(pPos,3)); var pMat=new THREE.PointsMaterial({color:c.accent,size:isMobile?0.08:0.06,transparent:true,opacity:0.6}); var particles=new THREE.Points(pGeo,pMat); scene.add(particles); // Connection lines var lineMax=isMobile?1200:2400; var lineGeo=new THREE.BufferGeometry(); var linePos=new Float32Array(lineMax*6); lineGeo.setAttribute('position',new THREE.BufferAttribute(linePos,3)); var lineMat=new THREE.LineBasicMaterial({color:c.line,transparent:true,opacity:0.15}); var lines=new THREE.LineSegments(lineGeo,lineMat); scene.add(lines); // Wireframe geometries var dodGeo=new THREE.DodecahedronGeometry(1.8,0); var dodMat=new THREE.MeshBasicMaterial({color:c.crema,wireframe:true,transparent:true,opacity:0.1}); var dod=new THREE.Mesh(dodGeo,dodMat); dod.position.set(-3,1,-2); scene.add(dod); var icoGeo=new THREE.IcosahedronGeometry(1.4,0); var icoMat=new THREE.MeshBasicMaterial({color:c.accent,wireframe:true,transparent:true,opacity:0.08}); var ico=new THREE.Mesh(icoGeo,icoMat); ico.position.set(4,-1.5,-3); scene.add(ico); var boxes=[]; for(var bi=0;bi<5;bi++){ var s=0.3+Math.random()*0.5; var bGeo=new THREE.BoxGeometry(s,s,s); var bMat=new THREE.MeshBasicMaterial({color:c.crema,wireframe:true,transparent:true,opacity:0.06}); var box=new THREE.Mesh(bGeo,bMat); box.position.set((Math.random()-0.5)*12,(Math.random()-0.5)*6,(Math.random()-0.5)*6); box.rotation.set(Math.random()*Math.PI,Math.random()*Math.PI,0); scene.add(box); boxes.push(box); } var fc=0; function animate(){ requestAnimationFrame(animate); fc++; var pos=pGeo.attributes.position.array; for(var ai=0;ai<pCount;ai++){ pos[ai*3]+=pVel[ai*3];pos[ai*3+1]+=pVel[ai*3+1];pos[ai*3+2]+=pVel[ai*3+2]; if(Math.abs(pos[ai*3])>10)pVel[ai*3]*=-1; if(Math.abs(pos[ai*3+1])>6)pVel[ai*3+1]*=-1; if(Math.abs(pos[ai*3+2])>5)pVel[ai*3+2]*=-1; } pGeo.attributes.position.needsUpdate=true; var li=0;var lp=lineGeo.attributes.position.array; for(var xi=0;xi<pCount&&li<lineMax;xi++){ for(var xj=xi+1;xj<pCount&&li<lineMax;xj++){ var dx=pos[xi*3]-pos[xj*3],dy=pos[xi*3+1]-pos[xj*3+1],dz=pos[xi*3+2]-pos[xj*3+2]; if(dx*dx+dy*dy+dz*dz<6){ lp[li*6]=pos[xi*3];lp[li*6+1]=pos[xi*3+1];lp[li*6+2]=pos[xi*3+2]; lp[li*6+3]=pos[xj*3];lp[li*6+4]=pos[xj*3+1];lp[li*6+5]=pos[xj*3+2]; li++; } } } for(var k=li*6;k<lineMax*6;k++)lp[k]=0; lineGeo.attributes.position.needsUpdate=true; lineGeo.setDrawRange(0,li*2); dod.rotation.x+=0.002;dod.rotation.y+=0.003; ico.rotation.x-=0.0015;ico.rotation.z+=0.002; boxes.forEach(function(b,bi){b.rotation.x+=0.001*(bi+1);b.rotation.y+=0.0015*(bi+1)}); cam.position.x=Math.sin(fc*0.0015)*1.5; cam.position.y=Math.cos(fc*0.001)*0.8; renderer.render(scene,cam); } animate(); // Theme observer new MutationObserver(function(){ c=getThemeColors();scene.background.set(c.bg);pMat.color.set(c.accent);lineMat.color.set(c.line);dodMat.color.set(c.crema);icoMat.color.set(c.accent);boxes.forEach(function(b){b.material.color.set(c.crema)}); }).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); window.addEventListener('resize',function(){cam.aspect=canvas.clientWidth/canvas.clientHeight;cam.updateProjectionMatrix();renderer.setSize(canvas.clientWidth,canvas.clientHeight)}); } }); </script> <script defer src="/components/cookie-consent.js"></script> <script defer src="/components/bottom-nav.js"></script> </body> </html>