Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server side rendering #3

Merged
merged 2 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ linters:
- misspell
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- predeclared
Expand Down
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This package based on the official Laravel adapter for Inertia.js: [inertiajs/in
- [x] Helpers for testing
- [x] Helpers for validation errors
- [x] Examples
- [ ] SSR
- [x] SSR

## Installation
Install using `go get` command:
Expand Down Expand Up @@ -139,8 +139,8 @@ i, err := inertia.New(
```go
i, err := inertia.New(
/* ... */
inertia.WithLogger(somelogger.New()),
// or inertia.WithoutLogger(),
inertia.WithLogger(), // default logger
inertia.WithLogger(somelogger.New()), // custom logger
)
```

Expand Down Expand Up @@ -249,6 +249,20 @@ ctx := inertia.WithValidationError(r.Context(), "some_field", "some error")
// pass it to the next middleware or inertia.Render function using r.WithContext(ctx).
```

#### SSR (Server Side Rendering) ([learn more](https://inertiajs.com/server-side-rendering))

To enable server side rendering you have provide an option on place where you initialize Gonertia:

```go
i, err := inertia.New(
/* ... */
inertia.WithSSR(), // if Node process is running on http://127.0.0.1:13714
inertia.WithSSR("http://127.0.0.1:1234"), // custom url
)
```

Also, you have to use asset bundling tools like [Vite](https://vitejs.dev/) or [Webpack](https://webpack.js.org/) (especially with [Laravel Mix](https://laravel-mix.com/)). The setup will vary depending on this choice, you can read more about it in [official docs](https://inertiajs.com/server-side-rendering) or check an example that works on [Vite](./examples/vue3_tailwind).

#### Testing

Of course, this package provides convenient interfaces for testing!
Expand All @@ -259,11 +273,11 @@ func TestHomepage(t *testing.T) {

// ...

assertable := inertia.Assert(t, body) // io.Reader body
assertable := inertia.Assert(t, body) // from io.Reader body
// OR
assertable := inertia.AssertFromBytes(t, body) // []byte body
assertable := inertia.AssertFromBytes(t, body) // from []byte body
// OR
assertable := inertia.AssertFromString(t, body) // string body
assertable := inertia.AssertFromString(t, body) // from string body

// now you can do assertions using assertable.Assert[...] methods:
assertable.AssertComponent("Foo/Bar")
Expand All @@ -276,6 +290,7 @@ func TestHomepage(t *testing.T) {
assertable.Version // foo bar
assertable.URL // https://example.com
assertable.Props // inertia.Props{"foo": "bar"}
assertable.Body // full response body
}
```

Expand Down
6 changes: 3 additions & 3 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func Test_TemplateDataFromContext(t *testing.T) {
if tt.wantErr && err == nil {
t.Fatal("error expected")
} else if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
} else if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
t.Fatalf("TemplateData=%#v, want=%#v", got, tt.want)
}
Expand Down Expand Up @@ -157,7 +157,7 @@ func Test_PropsFromContext(t *testing.T) {
if tt.wantErr && err == nil {
t.Fatal("error expected")
} else if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
} else if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
t.Fatalf("Props=%#v, want=%#v", got, tt.want)
}
Expand Down Expand Up @@ -276,7 +276,7 @@ func Test_ValidationErrorsFromContext(t *testing.T) {
if tt.wantErr && err == nil {
t.Fatal("error expected")
} else if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
} else if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
t.Fatalf("ValidationErrors=%#v, want=%#v", got, tt.want)
}
Expand Down
2 changes: 0 additions & 2 deletions examples/vue3_tailwind/Makefile

This file was deleted.

6 changes: 6 additions & 0 deletions examples/vue3_tailwind/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Golang + Vue3 + Vite + Tailwind + Inertia.js + Gonertia example

## How to run
1. `npm install`
2. `npm run build && node bootstrap/ssr/ssr.js`
3. `go run .`
5 changes: 1 addition & 4 deletions examples/vue3_tailwind/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ module github.com/romsar/gonertia/examples/vue3_tailwind

go 1.22

require (
github.com/romsar/gonertia v0.0.0
github.com/torenware/vite-go v0.5.6
)
require github.com/romsar/gonertia v0.0.0

replace github.com/romsar/gonertia => ../../
2 changes: 0 additions & 2 deletions examples/vue3_tailwind/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
github.com/torenware/vite-go v0.5.6 h1:4TrnG0lBOTESqE4nGzKgZTsmvgFnGvIcGQ6cRhdktuU=
github.com/torenware/vite-go v0.5.6/go.mod h1:tP33iI/kEQhR8TyowBjooxvp8kpHGA82eXuuI7apszc=
1 change: 1 addition & 0 deletions examples/vue3_tailwind/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func initInertia() *inertia.Inertia {
i, err := inertia.New(
"./resources/views/root.html",
inertia.WithVersionFromFile(manifestPath),
inertia.WithSSR(),
)
if err != nil {
log.Fatal(err)
Expand Down
8 changes: 4 additions & 4 deletions examples/vue3_tailwind/resources/views/root.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ vite "resources/css/app.css" }}">
{{ .inertiaHead }}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ vite "resources/css/app.css" }}">
{{ .inertiaHead }}
</head>

<body class="font-sans antialiased">
Expand Down
13 changes: 8 additions & 5 deletions helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package gonertia

import (
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
Expand All @@ -15,6 +17,7 @@ func I(opts ...func(i *Inertia)) *Inertia {
marshallJSON: json.Marshal,
sharedProps: make(Props),
sharedTemplateData: make(TemplateData),
logger: log.New(io.Discard, "", 0),
}

for _, opt := range opts {
Expand Down Expand Up @@ -151,30 +154,30 @@ func tmpFile(t *testing.T, content string) *os.File {

f, err := os.CreateTemp("", "gonertia")
if err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
}

closed := false

if _, err := f.WriteString(content); err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
}

if err := f.Close(); err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
}

closed = true

t.Cleanup(func() {
if !closed {
if err := f.Close(); err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
}
}

if err := os.Remove(f.Name()); err != nil {
t.Fatalf("unexpected error: %#v", err)
t.Fatalf("unexpected error: %s", err)
}
})

Expand Down
4 changes: 4 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func setJSONResponse(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
}

func setJSONRequest(r *http.Request) {
r.Header.Set("Content-Type", "application/json")
}

func setHTMLResponse(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html")
}
Expand Down
135 changes: 21 additions & 114 deletions inertia.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"encoding/json"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"strings"
)

// Inertia is a main Gonertia structure, which contains all the logic for being an Inertia adapter.
Expand All @@ -20,6 +20,9 @@ type Inertia struct {
sharedTemplateData TemplateData
sharedTemplateFuncs TemplateFuncs

ssrURL string
ssrHTTPClient *http.Client

containerID string
version string
marshallJSON marshallJSON
Expand All @@ -32,7 +35,7 @@ func New(rootTemplatePath string, opts ...Option) (*Inertia, error) {
rootTemplatePath: rootTemplatePath,
marshallJSON: json.Marshal,
containerID: "app",
logger: log.Default(),
logger: log.New(io.Discard, "", 0),
sharedProps: make(Props),
sharedTemplateData: make(TemplateData),
sharedTemplateFuncs: make(TemplateFuncs),
Expand All @@ -56,124 +59,28 @@ type logger interface {
Println(v ...any)
}

// Location creates redirect response.
//
// If request was made by Inertia - sets status to 409 and url will be in "X-Inertia-Location" header.
// Otherwise, it will do an HTTP redirect with specified status (default is 302 for GET, 303 for POST/PUT/PATCH).
func (i *Inertia) Location(w http.ResponseWriter, r *http.Request, url string, status ...int) {
if IsInertiaRequest(r) {
setInertiaLocationInResponse(w, url)
return
}

redirectResponse(w, r, url, status...)
}

// Back creates redirect response to the previous url.
func (i *Inertia) Back(w http.ResponseWriter, r *http.Request, status ...int) {
i.Location(w, r, i.backURL(r), status...)
}

// Render returns response with Inertia data.
//
// If request was made by Inertia - it will return data in JSON format.
// Otherwise, it will return HTML with root template.
func (i *Inertia) Render(w http.ResponseWriter, r *http.Request, component string, props ...Props) (err error) {
page, err := i.buildPage(r, component, firstOr[Props](props, nil))
if err != nil {
return fmt.Errorf("build page: %w", err)
}

if IsInertiaRequest(r) {
if err = i.doInertiaResponse(w, page); err != nil {
return fmt.Errorf("inertia response: %w", err)
}

return
}

if err = i.doHTMLResponse(w, r, page); err != nil {
return fmt.Errorf("html response: %w", err)
}

return nil
}

type page struct {
Component string `json:"component"`
Props Props `json:"props"`
URL string `json:"url"`
Version string `json:"version"`
}

func (i *Inertia) buildPage(r *http.Request, component string, props Props) (*page, error) {
props, err := i.prepareProps(r, component, props)
if err != nil {
return nil, fmt.Errorf("prepare props: %w", err)
}

return &page{
Component: component,
Props: props,
URL: r.RequestURI,
Version: i.version,
}, nil
// ShareProp adds passed prop to shared props.
func (i *Inertia) ShareProp(key string, val any) {
i.sharedProps[key] = val
}

func (i *Inertia) doInertiaResponse(w http.ResponseWriter, page *page) error {
pageJSON, err := i.marshallJSON(page)
if err != nil {
return fmt.Errorf("marshal page into json: %w", err)
}

setInertiaInResponse(w)
setJSONResponse(w)
setResponseStatus(w, http.StatusOK)

if _, err := w.Write(pageJSON); err != nil {
return fmt.Errorf("write bytes to response: %w", err)
}

return nil
// SharedProps returns shared props.
func (i *Inertia) SharedProps() Props {
return i.sharedProps
}

func (i *Inertia) doHTMLResponse(w http.ResponseWriter, r *http.Request, page *page) (err error) {
// If root template is already created - we'll use it to save some time.
if i.rootTemplate == nil {
i.rootTemplate, err = i.buildRootTemplate()
if err != nil {
return fmt.Errorf("build root template: %w", err)
}
}

templateData, err := i.buildTemplateData(r, page)
if err != nil {
return fmt.Errorf("build template data: %w", err)
}

setHTMLResponse(w)

if err := i.rootTemplate.Execute(w, templateData); err != nil {
return fmt.Errorf("execute root template: %w", err)
}

return nil
// SharedProp return the shared prop.
func (i *Inertia) SharedProp(key string) (any, bool) {
val, ok := i.sharedProps[key]
return val, ok
}

func (i *Inertia) inertiaContainerHTML(pageJSON []byte) template.HTML {
builder := new(strings.Builder)

// It doesn't look pretty, but fast!
builder.WriteString(`<div id="`)
builder.WriteString(i.containerID)
builder.WriteString(`" data-page="`)
template.HTMLEscape(builder, pageJSON)
builder.WriteString(`"></div>`)

return template.HTML(builder.String())
// ShareTemplateData adds passed data to shared template data.
func (i *Inertia) ShareTemplateData(key string, val any) {
i.sharedTemplateData[key] = val
}

func (i *Inertia) backURL(r *http.Request) string {
// At the moment, it based only on the "Referer" HTTP header.
return refererFromRequest(r)
// ShareTemplateFunc adds passed value to the shared template func map.
func (i *Inertia) ShareTemplateFunc(key string, val any) {
i.sharedTemplateFuncs[key] = val
}
Loading
Loading