Why Build from Scratch?
When I started blogging, I explored popular frameworks like Hugo, they’re fast, easy, and free to host on platforms like GitHub Pages or Cloudflare Pages. But I was more interested in the challenge of building things myself. I wanted to see how Go and React could work together in a real project.
For the blog’s UI, I used a-h/templ, which is great for Go-based templating. But as my UI grew, the templates became harder for me to manage. I initially built the dashboard separately in React, which meant I had to deploy the blog and dashboard independently, a process that quickly got tedious. Eventually, I decided to re-write the blog UI with React as well, for a more consistent and maintainable frontend.
That’s when I decided to embed the React build inside my Go Gin backend, so everything could be deployed together.
The Project Structure
other directories omitted for brevity
├── server
│ ├── router
│ │ ├── dist
│ │ │ └── index.html
│ │ └── frontend.go
│ └── server.go
Step-by-Step: Embedding React in Go Gin
1. Install the Static Middleware
go get github.com/gin-contrib/static
2. Embed the React Build
// frontend.go
package frontend
import (
"embed"
)
// Embed the entire "dist" directory, which includes index.html and assets
//
//go:embed "dist"
var embeddedFiles embed.FS
3. Expose the Embedded Files
func getFileSystem(path string) static.ServeFileSystem {
fs, err := static.EmbedFolder(embeddedFiles, path)
if err != nil {
log.Fatal(err)
}
return fs
}
4. Serve Static Files and Handle SPA Routing
func Serve(app *gin.Engine) {
distFS := getFileSystem("dist")
app.Use(static.Serve("/", distFS))
app.NoRoute(func(c *gin.Context) {
// Only serve index.html for non-API routes
if !strings.HasPrefix(c.Request.RequestURI, "/api") {
index, err := distFS.Open("index.html")
if err != nil {
log.Fatal(err)
}
defer index.Close()
stat, _ := index.Stat()
http.ServeContent(c.Writer, c.Request, "index.html", stat.ModTime(), index)
}
})
}
Why app.NoRoute()
is Crucial
When building a React single-page application (SPA), client-side routing means URLs are handled by React in the browser. If you refresh a route (like /dashboard
), the browser requests that path from the server. Without special handling, the server returns a 404 error.
The app.NoRoute()
handler solves this by:
-
Serving
index.html
for all non-API requests: This allows React to handle routing, so refreshing or deep-linking never breaks the app. -
Never serving HTML for API endpoints: By checking if the path starts with
/api
, you ensure API requests always return JSON or the expected response, never HTML.
The main job of
app.NoRoute()
here is to prevent refresh errors in your React app. It ensures that users never see a 404 when they refresh or navigate directly to a React route, while API endpoints remain unaffected and never return HTML.
Full Example: Putting It All Together
package frontend
import (
"embed"
"log"
"net/http"
"strings"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
//go:embed "dist"
var embeddedFiles embed.FS
func getFileSystem(path string) static.ServeFileSystem {
fs, err := static.EmbedFolder(embeddedFiles, path)
if err != nil {
log.Fatal(err)
}
return fs
}
func Serve(app *gin.Engine) {
distFS := getFileSystem("dist")
app.Use(static.Serve("/", distFS))
app.NoRoute(func(c *gin.Context) {
// Only serve index.html for non-API routes
if !strings.HasPrefix(c.Request.RequestURI, "/api") {
index, err := distFS.Open("index.html")
if err != nil {
log.Fatal(err)
}
defer index.Close()
stat, _ := index.Stat()
http.ServeContent(c.Writer, c.Request, "index.html", stat.ModTime(), index)
}
})
}
SEO Just Got Easier with React 19
One of my early concerns was SEO, especially since React apps are often client-rendered, and search engines need meta tags in the HTML head for proper indexing. Before React 19, you had to rely on libraries like React Helmet to inject meta tags dynamically, which added extra dependencies and complexity.
With React 19, this is now built in:
- You can define
<title>
,<meta>
, and<link>
tags directly in your React components. - React automatically hoists these tags to the
<head>
of the document, even if you declare them deep in your component tree. - No need for third-party SEO libraries or manual DOM manipulation.
Example:
// components/SEO.jsx
const SEO = ({ title, description }) => (
<>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="robots" content="index, follow" />
<link rel="canonical" href={window.location.href} />
</>
);
export default SEO;
https://github.com/joybiswas007/blog/blob/main/web/src/components/SEO.jsx
// Usage in a page/component
import SEO from "@/components/SEO";
const Archives = () => (
<>
<SEO title="Archives" description="description" />
{/* ...rest of your component */}
</>
);
React will automatically place these tags in the <head>
, making your dashboard and blog pages more SEO-friendly out of the box.
Takeaway
Building my blog from scratch with Go and React was both challenging and rewarding. Embedding the React dashboard in the backend made deployment much simpler, and understanding how to handle SPA routing with app.NoRoute()
was key to delivering a seamless user experience. Thanks to React 19’s built-in Document Metadata, SEO is now easier and doesn’t require extra libraries.
I also got help from the memos project’s frontend router implementation, which provided a valuable reference for embedding and serving frontend assets. They used Echo as the web framework, while I used Gin.
If you’re interested in the full source code or want to see exactly how I handled deployment, check out my blog repository, be sure to read the README for step-by-step deployment instructions.
Top comments (0)