DEV Community

Satria Nindhita
Satria Nindhita

Posted on

I Built a Pixiv Alternative in Go (and Taught It SEO)


Pixiv is wonderful. It's also, architecturally, a black box you can't learn from. So I built my own illustration community platform — Lumiina — live at https://www.lumiina.art, open source at Nyanns/lumiina.

The stack: Go (Gin + GORM) on the backend with strict Clean Architecture, PostgreSQL + Redis, and a React 19 + Vite SPA frontend. The interesting part wasn't the CRUD — it was everything around it: auth hardening, an image pipeline that doesn't melt your bandwidth bill, and making a JavaScript SPA actually rank on Google.

Here's what I learned.

1. SPAs are invisible to Google unless you fight for it

A client-rendered gallery is a ghost town for crawlers. I implemented bot pre-rendering: when the user-agent smells like Googlebot, the server returns fully rendered HTML with real <title>, meta/OG tags, and JSON-LD structured data per artwork — instead of an empty <div id="root">.

On top of that, every artwork publish fires an IndexNow ping, so Bing/Yandex learn about new content in minutes instead of weeks. Combined with a proper sitemap.xml and robots.txt, the site went from "exists" to "indexed" without waiting on Google's mercy.

Lesson: if your SPA needs SEO, server-side rendering isn't the only answer. Targeted bot pre-rendering is cheaper and keeps your SPA architecture intact.

2. Uploads are an attack surface — treat them like one

Anyone who's run a public upload endpoint knows the dread. My pipeline:

  • Magic-byte validation — never trust the extension or the Content-Type header.
  • Decompression bomb guard — decode config first, reject absurd dimensions before allocating a single pixel.
  • Client-side downsampling — a Canvas GPU pipeline shrinks 10–20 MB master illustrations to ~800 KB WebP before network transit. The server never sees the giant file, and neither does my Cloudinary bill.

Lesson: push expensive work to the client when you can. The cheapest byte to process is the one that never reaches your server.

3. Auth: boring, paranoid, and proud of it

  • bcrypt with constant-time dummy-hash execution so username enumeration via timing attacks doesn't work.
  • JWT (strict HS256) with Redis-backed revocation and a password-change epoch — changing your password kills every session, everywhere.
  • Rate limiting in layers, account lockout, and generic error messages that never reveal whether the username or the password was wrong.

None of this is novel. That's the point — auth should be boring. The novel bugs are the ones that end up on Have I Been Pwned.

4. IDs people can share

Artwork URLs use Sqids hash IDs (/artwork/v06sDC) instead of sequential integers. Short, non-guessable, and they don't leak "we have exactly 47 artworks" to competitors. Trivial to implement, surprisingly nice UX.

5. What I'd do differently

  • httpOnly cookies instead of localStorage JWT. It's the stronger default; the current setup works but cookies win on XSS resilience.
  • Start the content flywheel earlier. I over-invested in infrastructure before having content. A gallery with no art is a very fast empty room. Now there's a daily publishing rhythm — one new original artwork every day — and that's what actually moves the SEO needle. Tech gets you indexed; content gets you ranked.

The code

Everything is MIT-licensed: github.com/Nyanns/lumiina. The live site is lumiina.art.

If you're building a content platform in Go — or fighting SPA SEO — happy to answer questions in the comments. What would you have done differently?

Top comments (0)