diff --git a/README.md b/README.md
index d22178a..4bcc605 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ Configure the header with `CMS_SSO_HEADER` (default `X-SSO-Token`). Set `CMS_SSO
Because the backend trusts the forwarded header, it must only be reachable through the proxy. Bind it to the loopback interface or a private network, and make sure the proxy strips any client-supplied copy of that header.
+Anonymous visitors get a **Sign in with Google** button in the header. It redirects the browser to `VITE_SSO_LOGIN_URL` (frontend setting), where the provider authenticates the owner and returns to the page; the CMS itself never performs an OAuth exchange and holds no client secret. Leave the setting empty to hide the button when the proxy gates the whole app automatically.
+
## Content
Content supports one reusable category per document plus any number of badges. The anonymous `GET /api/content` endpoint accepts `category=engineering` and repeated `badge=go&badge=docker` filters; selected badges are combined with AND semantics. `GET /api/content/categories` returns the category dropdown values. The frontend applies these filters immediately when the category changes or when a badge is entered.
diff --git a/ui/app/.env.example b/ui/app/.env.example
index c9cdf36..1956b7d 100644
--- a/ui/app/.env.example
+++ b/ui/app/.env.example
@@ -1,8 +1,16 @@
# Leave empty in development; Vite proxies API requests to localhost:8080.
# Set this to the public backend URL when the frontend is hosted separately.
#
-# There is no sign-in endpoint: the upstream SSO proxy authenticates the owner
-# and the backend trusts the header it forwards.
+# The CMS has no sign-in endpoint of its own: the upstream SSO proxy
+# authenticates the owner and the backend trusts the header it forwards.
VITE_API_BASE_URL=
VITE_SESSION_ENDPOINT=/api/auth/session
VITE_CONTENT_ENDPOINT=/api/content
+
+# Google/IdP sign-in URL for the header button. The button redirects here, the
+# provider authenticates you, and you come back with the identity header set.
+# Leave empty to hide the button (for example when the proxy gates the app
+# automatically instead). Examples:
+# oauth2-proxy: /oauth2/start
+# hosted IdP: https://idp.example.com/login
+VITE_SSO_LOGIN_URL=
diff --git a/ui/app/README.md b/ui/app/README.md
index 8332d58..111294f 100644
--- a/ui/app/README.md
+++ b/ui/app/README.md
@@ -3,10 +3,16 @@
This Vite/React app lives in `ui/app` and is intentionally independent of the Go backend. Run
`npm install`, then `npm run dev` or `npm run build`.
-There is no sign-in screen. Content is public and read-only for anonymous visitors; the Edit
-button appears only when the backend reports `canEdit: true`, which happens when the upstream SSO
-proxy asserted the owner's identity on the request. `/api/auth/session` returns
-`{ "authenticated": bool, "canEdit": bool }` and never creates a session.
+There is no sign-in screen and no session is ever created. Content is public and read-only for
+anonymous visitors; the Edit button appears only when the backend reports `canEdit: true`, which
+happens when the upstream SSO proxy asserted the owner's identity on the request.
+`/api/auth/session` returns `{ "authenticated": bool, "canEdit": bool }`.
+
+Anonymous visitors also see a **Sign in with Google** button in the header. It redirects the
+browser to `VITE_SSO_LOGIN_URL` (with the current page passed as `rd`), where the identity
+provider authenticates you and sends you back with the identity header set. Leave
+`VITE_SSO_LOGIN_URL` empty to hide the button — useful when the proxy gates the whole app
+automatically instead. The app never performs an OAuth exchange, so it holds no client secret.
The API defaults to same-origin `/api/auth/session` and `/api/content`. Set `VITE_API_BASE_URL`
for a separate backend, or override the endpoint paths with `VITE_SESSION_ENDPOINT` and
diff --git a/ui/app/src/App.tsx b/ui/app/src/App.tsx
index ebfbca6..8d88400 100644
--- a/ui/app/src/App.tsx
+++ b/ui/app/src/App.tsx
@@ -14,7 +14,7 @@ import {
import {useAuth} from './auth'
function Shell({children}: { children: React.ReactNode }) {
- const {authenticated, loading} = useAuth()
+ const {authenticated, loading, canSignIn, signIn} = useAuth()
const [dark, setDark] = useState(() => window.localStorage.getItem('urpi-theme') !== 'light')
useEffect(() => {
document.documentElement.dataset.theme = dark ? 'dark' : 'light'
@@ -25,6 +25,9 @@ function Shell({children}: { children: React.ReactNode }) {
backlog_linux · go · java · ideas