
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-Typeheader. - 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)