Nëse kanvasi është faqja jote kryesore, nuk ke faqe kryesore.
Si ta bësh një përvojë WebGL me rrëshqitje të indeksueshme pa hequr kanvasin — dhe pse faqja jote e uljes nuk duhet të ketë nevojë për JavaScript që të ekzistojë.
If Googlebot needs to run your canvas to read your value proposition, you don't have a landing page. You have a screensaver.
That's the position amalbryx.com was in until last month. The site is a Three.js WebGL scroll experience — visual, slick, the kind of thing that looks like a studio should look. But it was rendered client-side only. Next.js with `dynamic({ ssr: false })` shipped an empty shell: a few metadata tags, a `<noscript>` fallback, and nothing else. Crawlers saw void. The hero copy, the services, the case studies, the call-to-action — none of it was there.
A lot of studios make this mistake. The fix sounds easy — enable SSR, let the component render on the server, done. But Three.js is a browser thing. It doesn't run in Node. So the obvious move fails immediately.
The real fix is progressive enhancement: render the content as real, crawlable HTML, then let the WebGL layer enhance on top. Here's what that takes in practice.
The core problem: one eager import traps the whole route
When you `import * as THREE from 'three'` at the top of a component file, that import is eager. The bundler pulls it into the module graph, and the route inherits it. One eager import of a browser-only library at module scope, and the whole page has to be client-only.
The pattern most people try next: pull Three into its own component and wrap that in `dynamic({ ssr: false })`. Better — but if your content lives inside that client subtree, it still isn't server-rendered, and the `ssr: false` boundary still ships an empty placeholder to the crawler.
The unglamorous truth: you can't eagerly import Three in a component you want to server-render. So don't.
The fix: type-only import, then lazy-load at mount
Split the import in two. Bring in the Three type definitions as types only — they compile away and just tell TypeScript what the shapes are. Then import the runtime module lazily, inside the `useEffect` where the canvas actually mounts.
import type * as THREE from "three";
export function HomepageHero() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
// runtime import — client-only, code-split, off the SSR path
import("three").then((Three) => {
const scene = new Three.Scene();
// ...rest of the engine setup
});
}, []);
return <canvas ref={canvasRef} />;
}Now the component server-renders. The `<canvas>` tag and the surrounding section markup reach the crawler as real HTML. The Three.js bundle stays in a client chunk and only loads in the browser. The part to get right: make sure the server-rendered markup hydrates cleanly and the engine initializes once, not twice.
The rest of the pass: content first, enhancement second
Once the component was SSR-safe, the rest was standard practice. We rewrote the fallback so a `<noscript>` rule reveals the same scroll-animated copy the canvas fades in — no JavaScript, and the visitor still reads the text as static HTML. We dropped a redundant layout `<noscript>` that carried its own competing `<h1>`. Then the broader pass: schema markup for CreativeWork (case studies), BreadcrumbList (navigation), BlogPosting (notes), and FAQPage. Real `/services` and `/contact` pages with their own copy. A sitemap. Hreflang for the Albanian and Arabic versions. The hard part was the component; once that shipped, the rest was pattern work.
None of it required removing the WebGL experience. The canvas is still there. The scroll animations still fire. But the canvas is decorative now, not load-bearing — the information architecture lives in real HTML, and the canvas enhances it.
Why this pattern matters
Google renders JavaScript now. Bing is inconsistent about it. LinkedIn's link crawler doesn't run it at all. If you care about reach, you're optimizing for the crawlers that don't wait.
There's an accessibility angle too. If your copy only lives in a canvas, screen readers get nothing and keyboard users have nowhere to go. The same fallback that fixes crawlers — real HTML for content, canvas for enhancement — fixes that as well.
"Progressive enhancement isn't nostalgia. It's how a marketing site survives the cases where JavaScript doesn't load, crawlers don't wait, and the visitor just wants the information."
The cost and the payoff
The core fix — lazy import plus server-render — was about half a day. The broader SEO pass landed alongside it. For a site that wasn't being indexed on its own core queries, that's cheap.
The payoff is threefold. The site indexes properly — hero copy, services, case-study titles all reachable. The visual still works for everyone with JavaScript, and everyone without it still gets the information. And the repo is simpler: a smaller component, a clearer mental model, less for the next engineer to untangle.
The hard part was never the code. It's saying no to a class of design — the kind that insists the canvas render before the content exists. Games, real-time collaborative spaces, some SPAs: the full-canvas model is right for them. For a marketing site, it's wrong.
Ship the content first. Let the canvas enhance it. Your crawlers will thank you. Your keyboard users will thank you. And you won't spend six months wondering why the shiny new site isn't ranking.
Nuk je gati për brif? Qëndro në qarkullim.
Një shënim i shkurtër kur dorëzojmë diçka të re. Pa newsletter, pa fushatë drip, pa spam.
Vetëm email. Pa tracking pixels. Na shkruani dhe ju heqim nga lista.
