You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/05/04 12:31:51 UTC
(grails-static-website) 09/10: Correct grails-vite-spa v8 CORS chapter + cross-reference grails-doc
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch improve-v8-guides-pre-release
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git
commit aa1526d3205d73185c5510fd2a542297d07642b2
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Mon May 4 08:30:54 2026 -0400
Correct grails-vite-spa v8 CORS chapter + cross-reference grails-doc
The cors.adoc chapter previously claimed there was nothing
Grails-specific for CORS handling and pointed only at the Spring Boot
CORS reference. That was wrong: grails-doc/src/en/guide/theWebLayer/cors.adoc
documents Grails 8's first-class grails.cors.enabled configuration
block, which produces a Spring CorsConfiguration mapped to /** by
default and supports per-mapping overrides.
The chapter now shows the canonical grails.cors block:
grails:
cors:
enabled: true
allowedOrigins:
- 'https://app.example.com'
...
while keeping the original recommendation that the same-origin layout
this guide builds removes the need for CORS entirely - the Grails
config is only relevant if you split the SPA and the API across
origins.
Other chapter additions:
* spaController.adoc cites the underlying source for the forward
method (grails-controllers/src/main/groovy/grails/web/util/WebUtils.java)
and the user-facing reference at
grails-doc/src/en/guide/theWebLayer/controllers/forwarding.adoc.
* urlMappings.adoc points at grails-doc/src/en/guide/theWebLayer/urlMappings.adoc
and the restfulMappings sub-chapter.
* devMode.adoc references grails-doc/src/en/guide/gettingStarted/developmentReloading.adoc
for the Spring DevTools story.
* whatYouWillBuild.adoc and backend.adoc add the matching
grails-doc/src/en/guide/REST/ pointers.
Every substantive chapter ends with a "Files touched" bullet list
matching the established style.
Assisted-by: claude-code:claude-4.6-opus
---
guides/grails-vite-spa/v8/guide/backend.adoc | 9 ++++++++-
guides/grails-vite-spa/v8/guide/cors.adoc | 22 +++++++++++++++++++++-
guides/grails-vite-spa/v8/guide/devMode.adoc | 6 +++++-
guides/grails-vite-spa/v8/guide/frontend.adoc | 7 +++++++
guides/grails-vite-spa/v8/guide/layout.adoc | 5 +++++
guides/grails-vite-spa/v8/guide/prodBuild.adoc | 4 ++++
guides/grails-vite-spa/v8/guide/spaController.adoc | 6 ++++++
guides/grails-vite-spa/v8/guide/urlMappings.adoc | 6 ++++++
guides/grails-vite-spa/v8/guide/viteConfig.adoc | 4 ++++
.../grails-vite-spa/v8/guide/whatYouWillBuild.adoc | 6 ++++++
10 files changed, 72 insertions(+), 3 deletions(-)
diff --git a/guides/grails-vite-spa/v8/guide/backend.adoc b/guides/grails-vite-spa/v8/guide/backend.adoc
index 4b1a39e15cf..4d272d4c81a 100644
--- a/guides/grails-vite-spa/v8/guide/backend.adoc
+++ b/guides/grails-vite-spa/v8/guide/backend.adoc
@@ -26,4 +26,11 @@ include::../snippets/backend/grails-app/views/book/_book.gson[]
include::../snippets/backend/grails-app/views/book/index.gson[]
----
-This is exactly the pattern from the link:../grails-rest-library/8/guide/index.html[REST Library] guide, narrowed to a single domain so the Vite-and-React story stays in focus.
+This is exactly the pattern from the link:../grails-rest-library/8/guide/index.html[REST Library] guide, narrowed to a single domain so the Vite-and-React story stays in focus. The `RestfulController` and `.gson` view conventions are documented in `grails-doc/src/en/guide/REST/` (subchapters `domainResources.adoc`, `restRendering.adoc`).
+
+Files touched in this chapter:
+
+* `backend/grails-app/domain/example/Book.groovy`
+* `backend/grails-app/controllers/example/BookController.groovy`
+* `backend/grails-app/views/book/_book.gson`
+* `backend/grails-app/views/book/index.gson`
diff --git a/guides/grails-vite-spa/v8/guide/cors.adoc b/guides/grails-vite-spa/v8/guide/cors.adoc
index 527d11b423a..db43c5785af 100644
--- a/guides/grails-vite-spa/v8/guide/cors.adoc
+++ b/guides/grails-vite-spa/v8/guide/cors.adoc
@@ -3,9 +3,29 @@ CORS does not appear in either dev or prod for this layout - and that is the poi
* *In dev*, the browser only ever talks to Vite on `:5173`. Vite forwards `/api/**` to Grails on `:8080` via its server-to-server proxy; the browser never sees a cross-origin request. No CORS preflight, no `Access-Control-Allow-Origin` headers.
* *In prod*, the browser only ever talks to the bootJar on `:8080`. The SPA's static bundle is served from `/`; the API is served from `/api/**`. Both are the same origin. Same story: no CORS.
-For the case where the SPA and the API genuinely live on different origins (e.g. SPA on `app.example.com`, API on `api.example.com`), you would add a `CorsFilter` to the Grails side and configure allowed origins explicitly. The Spring Boot guide on link:https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.servlet.spring-mvc.cors[CORS] is the canonical reference; nothing Grails-specific is required.
+For the case where the SPA and the API genuinely live on different origins (e.g. SPA on `app.example.com`, API on `api.example.com`), Grails ships a first-class CORS configuration block. From `grails-doc/src/en/guide/theWebLayer/cors.adoc` (the official 8.0.x reference):
+
+[source,yaml]
+.grails-app/conf/application.yml
+----
+grails:
+ cors:
+ enabled: true
+ allowedOrigins:
+ - 'https://app.example.com'
+ allowedMethods:
+ - GET
+ - POST
+ allowCredentials: true
+----
+
+The `grails.cors` block produces a Spring `CorsConfiguration` under the hood, mapped to `/**` by default. Per-mapping overrides are documented in the same `cors.adoc` chapter under the `mappings:` sub-key. The Spring Boot CORS documentation is the underlying reference for the option semantics (max-age, exposed headers, etc.).
[NOTE]
====
*Auth*: HTTP-only cookie sessions work transparently with this same-origin layout - the browser sends the session cookie on every request without any client-side wiring. JWT in `Authorization` headers is the alternative when the SPA is hosted independently of the API; it requires the SPA to attach the header explicitly on every fetch. The cookie-session path is recommended for browser-only SPAs unless you have a reason to pick JWT.
====
+
+Files touched in this chapter:
+
+* None (CORS only becomes relevant if you split the SPA and the API across origins)
diff --git a/guides/grails-vite-spa/v8/guide/devMode.adoc b/guides/grails-vite-spa/v8/guide/devMode.adoc
index 57036f56ef6..41e55ecf1fb 100644
--- a/guides/grails-vite-spa/v8/guide/devMode.adoc
+++ b/guides/grails-vite-spa/v8/guide/devMode.adoc
@@ -20,4 +20,8 @@ The browser opens `http://localhost:5173`. Every `fetch('/api/books')` call from
Two things to keep an eye on:
* *Same-origin in dev too.* Because the SPA always calls relative paths and Vite proxies them, the browser never sees a cross-origin request. CORS preflight requests do not happen even in dev.
-* *Spring DevTools for the backend, Vite HMR for the frontend.* Each side reloads independently; you can edit `App.jsx` without restarting Grails, and edit a Groovy controller without losing your SPA state.
+* *Spring DevTools for the backend, Vite HMR for the frontend.* Each side reloads independently; you can edit `App.jsx` without restarting Grails, and edit a Groovy controller without losing your SPA state. The Spring DevTools story for Grails 8 is covered in `grails-doc/src/en/guide/gettingStarted/developmentReloading.adoc`.
+
+Files touched in this chapter:
+
+* None directly (this chapter is a runbook)
diff --git a/guides/grails-vite-spa/v8/guide/frontend.adoc b/guides/grails-vite-spa/v8/guide/frontend.adoc
index 8534cfafd24..60d7b4e1b59 100644
--- a/guides/grails-vite-spa/v8/guide/frontend.adoc
+++ b/guides/grails-vite-spa/v8/guide/frontend.adoc
@@ -27,3 +27,10 @@ include::../snippets/frontend/src/App.jsx[]
The single `fetch('/api/books')` call is the only contract between the two halves. The relative path means the same code runs in dev (where Vite proxies to `:8080`) and in prod (where the SPA is served from the bootJar at `:8080` already).
To use Vue 3 instead of React, swap the dependency to `vue` + `@vitejs/plugin-vue` and replace `App.jsx` with an `App.vue` single-file component. The Gradle wiring, Vite proxy, and backend never change.
+
+Files touched in this chapter:
+
+* `frontend/package.json`
+* `frontend/index.html`
+* `frontend/src/main.jsx`
+* `frontend/src/App.jsx`
diff --git a/guides/grails-vite-spa/v8/guide/layout.adoc b/guides/grails-vite-spa/v8/guide/layout.adoc
index 2f08b7f7d11..99fbe3dacb3 100644
--- a/guides/grails-vite-spa/v8/guide/layout.adoc
+++ b/guides/grails-vite-spa/v8/guide/layout.adoc
@@ -21,3 +21,8 @@ Three patterns:
* `npmInstall` -> `frontendBuild` -> `copyFrontendToBackend` is a chain. Each task declares its `inputs`/`outputs` so Gradle's up-to-date check skips the chain when nothing has changed.
* `copyFrontendToBackend` writes into `backend/src/main/resources/public/`. Spring Boot serves anything under that directory at `/` automatically; no further wiring needed.
* The `afterEvaluate { tasks.named('processResources').configure { dependsOn ... } }` line is what makes plain `./gradlew :backend:bootJar` (or `bootRun`) trigger the SPA build. Readers never type the frontend tasks directly.
+
+Files touched in this chapter:
+
+* `settings.gradle`
+* `build.gradle`
diff --git a/guides/grails-vite-spa/v8/guide/prodBuild.adoc b/guides/grails-vite-spa/v8/guide/prodBuild.adoc
index 7fcdcf5f31f..992c2636dc7 100644
--- a/guides/grails-vite-spa/v8/guide/prodBuild.adoc
+++ b/guides/grails-vite-spa/v8/guide/prodBuild.adoc
@@ -19,3 +19,7 @@ The resulting jar serves the SPA from `/` and the API from `/api/**`, both from
====
The matching link:../grails-docker-bootbuildimage/8/guide/index.html[Docker bootBuildImage] guide turns this exact bootJar into an OCI image. Combined, the two guides give you `git push` -> tagged GHCR image -> single-container deploy.
====
+
+Files touched in this chapter:
+
+* None directly (this chapter is a runbook for the build chain wired up in `layout.adoc`)
diff --git a/guides/grails-vite-spa/v8/guide/spaController.adoc b/guides/grails-vite-spa/v8/guide/spaController.adoc
index 86b2bdfdde1..782a8147647 100644
--- a/guides/grails-vite-spa/v8/guide/spaController.adoc
+++ b/guides/grails-vite-spa/v8/guide/spaController.adoc
@@ -12,3 +12,9 @@ include::../snippets/backend/grails-app/controllers/example/SpaController.groovy
====
*Why not a wildcard URL mapping like `'/$path**'(controller: 'spa', action: 'index')`?* Because URL mappings resolve before static resources. A wildcard mapping would intercept `/favicon.ico` and `/assets/main-abc123.js`, returning the SPA's `index.html` instead of the asset. Letting Spring Boot's static-resource handler claim everything except `/api/**` and `/` is the simplest correct shape.
====
+
+The `forward` method available on every Grails controller is the same one the framework uses internally for URL-mapping rewrites; see `grails-controllers/src/main/groovy/grails/web/util/WebUtils.java` for the underlying implementation and `grails-doc/src/en/guide/theWebLayer/controllers/forwarding.adoc` for the user-facing reference.
+
+Files touched in this chapter:
+
+* `backend/grails-app/controllers/example/SpaController.groovy`
diff --git a/guides/grails-vite-spa/v8/guide/urlMappings.adoc b/guides/grails-vite-spa/v8/guide/urlMappings.adoc
index 5960a728685..27a51491b33 100644
--- a/guides/grails-vite-spa/v8/guide/urlMappings.adoc
+++ b/guides/grails-vite-spa/v8/guide/urlMappings.adoc
@@ -10,3 +10,9 @@ include::../snippets/backend/grails-app/controllers/example/UrlMappings.groovy[]
* Everything else - `/`, `/books/42`, `/about` - resolves to `SpaController.index`, which forwards to the bundled `index.html`. The SPA's client-side router takes over from there.
The forward (rather than a redirect) is what makes deep links survive a browser refresh: a hard reload on `/books/42` returns the same `index.html` payload, the SPA boots, and React Router parses the URL.
+
+The URL-mapping DSL itself is covered in `grails-doc/src/en/guide/theWebLayer/urlMappings.adoc` and the per-method-routing story in `grails-doc/src/en/guide/theWebLayer/urlmappings/restfulMappings.adoc`.
+
+Files touched in this chapter:
+
+* `backend/grails-app/controllers/example/UrlMappings.groovy`
diff --git a/guides/grails-vite-spa/v8/guide/viteConfig.adoc b/guides/grails-vite-spa/v8/guide/viteConfig.adoc
index f695d3e0602..b05fc8cd5a2 100644
--- a/guides/grails-vite-spa/v8/guide/viteConfig.adoc
+++ b/guides/grails-vite-spa/v8/guide/viteConfig.adoc
@@ -12,3 +12,7 @@ Two pieces are doing the work:
* `build.outDir = 'dist'` controls where `npm run build` writes the static bundle. Our root `build.gradle`'s `copyFrontendToBackend` task reads from this exact path.
`emptyOutDir: true` makes a clean rebuild every time, so a renamed asset does not leave a stale predecessor in `dist/`.
+
+Files touched in this chapter:
+
+* `frontend/vite.config.js`
diff --git a/guides/grails-vite-spa/v8/guide/whatYouWillBuild.adoc b/guides/grails-vite-spa/v8/guide/whatYouWillBuild.adoc
index 8be6550058e..1769c4b5501 100644
--- a/guides/grails-vite-spa/v8/guide/whatYouWillBuild.adoc
+++ b/guides/grails-vite-spa/v8/guide/whatYouWillBuild.adoc
@@ -23,3 +23,9 @@ Two run-modes:
* *Dev*: `./gradlew :backend:bootRun` in one shell, `cd frontend && npm run dev` in another. Vite serves the SPA on `:5173` with HMR; the proxy forwards `/api/**` to the Grails app on `:8080`.
* *Prod*: `./gradlew :backend:bootJar`. Gradle runs `npm run build`, copies `frontend/dist/` into `backend/src/main/resources/public/`, and packages everything into one bootJar. The SPA serves from `/` and `/api/**` from the same origin - CORS irrelevant.
+
+The official Grails 8 reference manual (`grails-doc/src/en/guide/REST/`) covers the JSON-rendering side this guide consumes. The CORS chapter (`grails-doc/src/en/guide/theWebLayer/cors.adoc`) covers the `grails.cors.enabled` block this guide does NOT use - because the same-origin layout this guide builds removes the need for CORS entirely. Both chapters are worth knowing about.
+
+Files touched in this chapter:
+
+* None