diff --git a/playground/docus/content/2.studio/2.setup.md b/playground/docus/content/2.studio/2.setup.md
deleted file mode 100644
index 9c26426c..00000000
--- a/playground/docus/content/2.studio/2.setup.md
+++ /dev/null
@@ -1,151 +0,0 @@
----
-title: Setup
-description: Learn how to install and configure Nuxt Studio to edit your content in production with GitHub authentication.
-navigation:
- title: Setup
- icon: i-lucide-settings
-seo:
- title: Nuxt Studio Setup Guide
- description: Learn how to install and configure the self-hosted Nuxt Studio module for your Nuxt Content website. Edit content in production with GitHub authentication.
----
-
-::tip{to="https://nuxt.studio/docs/setup"}
-This documentation covers only the new open-source Nuxt Studio module.
-Click here to view the documentation for the legacy standalone platform.
-::
-
-## Install
-
-Add the Nuxt Studio module to your project:
-
-::code-group
-```bash [pnpm]
-pnpm add nuxt-studio
-```
-
-```bash [npm]
-npm install nuxt-studio
-```
-
-```bash [yarn]
-yarn add nuxt-studio
-```
-
-```bash [bun]
-bun add nuxt-studio
-```
-::
-
-Alternatively, use the Nuxt CLI to automatically add the module:
-
-```bash
-npx nuxi module add nuxt-studio
-```
-
-## Configure
-
-Add the module to your `nuxt.config.ts` and configure your GitHub repository:
-
-```ts [nuxt.config.ts]
-export default defineNuxtConfig({
- modules: [
- '@nuxt/content',
- 'nuxt-studio'
- ],
-
- studio: {
- // Studio admin route (default: '/_studio')
- route: '/_studio',
-
- // GitHub repository configuration (required)
- repository: {
- provider: 'github', // only GitHub is currently supported
- owner: 'your-username', // your GitHub username or organization
- repo: 'your-repo', // your repository name
- branch: 'main', // the branch to commit to
- rootDir: '', // optional: if your Nuxt app is in a subdirectory
- private: true // optional: whether the repository is private or public (default: true)
- }
- }
-})
-```
-
-::prose-tip
-If your Nuxt Content application is in a monorepo or subdirectory, specify the `rootDir` option to point to the correct location.
-::
-
-## Create GitHub OAuth App
-
-Nuxt Studio uses GitHub OAuth for authentication.
-
-::prose-steps
-> If you changed the `route` option in your config, update the callback URL accordingly (the route set instead of `/studio`)
-
-### Navigate to GitHub Developer Settings
-
-Go to [GitHub Developer Settings](https://github.com/settings/developers) and click **New OAuth App**
-
-### Configure the OAuth Application
-
-Fill in the required fields:
-
-- **Application name**: Your app name
-- **Homepage URL**: Your production website homepage URL
-- **Authorization callback URL**: `https://yourdomain.com/_studio/auth/github`:tip [If you want to try studio on project running in local, you can simply set the callback url to your local url `http://localhost:3000`.]
-
-### Copy Your Credentials
-
-After creating the OAuth app, you'll receive:
-
-- A **Client ID** (visible immediately)
-- A **Client Secret** (click **Generate a new client secret**)
-
-### Set your environment Variables
-
-Add the GitHub OAuth credentials to your deployment platform's environment variables or in your `.env` file in local
-
-```bash [.env]
-STUDIO_GITHUB_CLIENT_ID=your_github_client_id
-STUDIO_GITHUB_CLIENT_SECRET=your_github_client_secret
-```
-::
-
-## Deployment
-
-::warning
-The new Nuxt Studio module requires a server-side route for authentication.
-While static generation remains supported with [Nuxt hybrid rendering](https://nuxt.com/docs/4.x/guide/concepts/rendering#route-rules), your site must be **deployed on a platform that supports server-side rendering (SSR)**.
-::
-
-### Accessing Studio
-
-After deployment, access the Studio interface by navigating to your configured route (default: `/_studio`):
-
-1. Click **Login with GitHub** if it does not directly redirect to the OAuth app authorization page
-2. Authorize the OAuth application
-3. You'll be redirected back to Studio, ready to edit your content
-
-::prose-note
-Secure OAuth-based login with **Google** should be available quickly in the Beta release.
-::
-
-## Development mode
-
-Nuxt Studio includes an **experimental** development mode that enables real-time file system synchronization:
-
-```ts [nuxt.config.ts]
-export default defineNuxtConfig({
- studio: {
- development: {
- sync: true // Enable development mode
- }
- }
-})
-```
-
-When enabled, Nuxt Studio will:
-
-- ✅ Write changes directly to your local `content/` directory
-- ✅ Write media changes to your local `public/` directory
-- ❌ Listen for file system changes and update the editor
-- ❌ Commit changes to your repository (use your classical workflow instead)
diff --git a/src/app/src/components/AppFooter.vue b/src/app/src/components/AppFooter.vue
index 33c0dc97..b2c98a39 100644
--- a/src/app/src/components/AppFooter.vue
+++ b/src/app/src/components/AppFooter.vue
@@ -2,9 +2,10 @@
import { computed } from 'vue'
import { useStudio } from '../composables/useStudio'
import { useStudioState } from '../composables/useStudioState'
+import type { DropdownMenuItem } from '@nuxt/ui'
const { ui, host, git } = useStudio()
-const { preferences, updatePreference, unsetActiveLocation } = useStudioState()
+const { devMode, preferences, updatePreference, unsetActiveLocation } = useStudioState()
const user = host.user.get()
// const showTechnicalMode = computed({
@@ -16,19 +17,19 @@ const user = host.user.get()
const repositoryUrl = computed(() => git.getBranchUrl())
const userMenuItems = computed(() => [
- [
- // [{
- // slot: 'view-mode' as const,
- // }
- repositoryUrl.value
- ? {
+ repositoryUrl.value
+ ? [
+ // [{
+ // slot: 'view-mode' as const,
+ // }
+ {
label: `${host.repository.owner}/${host.repository.repo}`,
icon: 'i-simple-icons:github',
to: repositoryUrl.value,
target: '_blank',
- }
- : undefined,
- ].filter(Boolean),
+ },
+ ]
+ : undefined,
[{
label: 'Sign out',
icon: 'i-lucide-log-out',
@@ -38,7 +39,7 @@ const userMenuItems = computed(() => [
})
},
}],
-])
+].filter(Boolean) as DropdownMenuItem[][])
function closeStudio() {
unsetActiveLocation()
@@ -50,7 +51,14 @@ function closeStudio() {
+
+ Using local filesystem
+
diff --git a/src/app/src/composables/useContext.ts b/src/app/src/composables/useContext.ts
index 9f22dc94..337d363a 100644
--- a/src/app/src/composables/useContext.ts
+++ b/src/app/src/composables/useContext.ts
@@ -234,6 +234,10 @@ export const useContext = createSharedComposable((
actionInProgress.value = null
}
+ function switchFeature(feature: StudioFeature) {
+ router.push(`/${feature}`)
+ }
+
return {
currentFeature,
activeTree,
@@ -246,5 +250,6 @@ export const useContext = createSharedComposable((
draftCount,
isDraftInProgress,
unsetActionInProgress,
+ switchFeature,
}
})
diff --git a/src/app/src/composables/useDraftBase.ts b/src/app/src/composables/useDraftBase.ts
index 0d35cec3..a3eed97f 100644
--- a/src/app/src/composables/useDraftBase.ts
+++ b/src/app/src/composables/useDraftBase.ts
@@ -2,12 +2,12 @@ import type { Storage } from 'unstorage'
import { joinURL } from 'ufo'
import type { DraftItem, StudioHost, GithubFile, DatabaseItem, MediaItem, BaseItem } from '../types'
import { ContentFileExtension } from '../types'
-import { studioFlags } from './useStudio'
import { DraftStatus } from '../types/draft'
import { checkConflict, findDescendantsFromFsPath } from '../utils/draft'
import type { useGit } from './useGit'
import { useHooks } from './useHooks'
import { ref } from 'vue'
+import { useStudioState } from './useStudioState'
export function useDraftBase(
type: 'media' | 'document',
@@ -25,6 +25,7 @@ export function useDraftBase(
const areDocumentsEqual = host.document.utils.areEqual
const hooks = useHooks()
+ const { devMode } = useStudioState()
async function get(fsPath: string): Promise | undefined> {
return list.value.find(item => item.fsPath === fsPath) as DraftItem
@@ -73,40 +74,45 @@ export function useDraftBase(
await storage.removeItem(fsPath)
await hostDb.delete(fsPath)
- let deleteDraftItem: DraftItem | null = null
- if (existingDraftItem) {
- if (existingDraftItem.status === DraftStatus.Deleted) return
+ if (!devMode.value) {
+ let deleteDraftItem: DraftItem | null = null
+ if (existingDraftItem) {
+ if (existingDraftItem.status === DraftStatus.Deleted) return
- if (existingDraftItem.status === DraftStatus.Created) {
- list.value = list.value.filter(item => item.fsPath !== fsPath)
+ if (existingDraftItem.status === DraftStatus.Created) {
+ list.value = list.value.filter(item => item.fsPath !== fsPath)
+ }
+ else {
+ // TODO: check if gh file has been updated
+ const githubFile = await git.fetchFile(joinURL('content', fsPath), { cached: true }) as GithubFile
+
+ deleteDraftItem = {
+ fsPath: existingDraftItem.fsPath,
+ status: DraftStatus.Deleted,
+ original: existingDraftItem.original,
+ githubFile,
+ }
+
+ list.value = list.value.map(item => item.fsPath === fsPath ? deleteDraftItem! : item) as DraftItem[]
+ }
}
else {
+ // TODO: check if gh file has been updated
+ const githubFile = await git.fetchFile(joinURL('content', fsPath), { cached: true }) as GithubFile
+
deleteDraftItem = {
- fsPath: existingDraftItem.fsPath,
+ fsPath,
status: DraftStatus.Deleted,
- original: existingDraftItem.original,
- githubFile: existingDraftItem.githubFile,
+ original: originalDbItem,
+ githubFile,
}
- list.value = list.value.map(item => item.fsPath === fsPath ? deleteDraftItem! : item) as DraftItem[]
+ list.value.push(deleteDraftItem)
}
- }
- else {
- // TODO: check if gh file has been updated
- const githubFile = await git.fetchFile(joinURL('content', fsPath), { cached: true }) as GithubFile
-
- deleteDraftItem = {
- fsPath,
- status: DraftStatus.Deleted,
- original: originalDbItem,
- githubFile,
- }
-
- list.value.push(deleteDraftItem)
- }
- if (deleteDraftItem) {
- await storage.setItem(fsPath, deleteDraftItem)
+ if (deleteDraftItem) {
+ await storage.setItem(fsPath, deleteDraftItem)
+ }
}
if (rerender) {
@@ -215,7 +221,7 @@ export function useDraftBase(
}
function getStatus(modified: BaseItem, original: BaseItem): DraftStatus {
- if (studioFlags.dev) {
+ if (devMode.value) {
return DraftStatus.Pristine
}
diff --git a/src/app/src/composables/useStudio.ts b/src/app/src/composables/useStudio.ts
index 5e6742e6..76edecaa 100644
--- a/src/app/src/composables/useStudio.ts
+++ b/src/app/src/composables/useStudio.ts
@@ -7,20 +7,20 @@ import { useDraftMedias } from './useDraftMedias'
import { ref } from 'vue'
import { useTree } from './useTree'
import type { RouteLocationNormalized } from 'vue-router'
-import type { StudioHost, GitOptions, DatabaseItem } from '../types'
+import type { GitOptions, DatabaseItem } from '../types'
import { StudioFeature } from '../types'
import { documentStorage, mediaStorage, nullStorageDriver } from '../utils/storage'
import { useHooks } from './useHooks'
import { useStudioState } from './useStudioState'
-export const studioFlags = {
- dev: false,
-}
-
export const useStudio = createSharedComposable(() => {
const isReady = ref(false)
const host = window.useStudioHost()
- studioFlags.dev = host.meta.dev
+ const { devMode, enableDevMode, preferences, setManifestId } = useStudioState()
+
+ if (host.meta.dev) {
+ enableDevMode()
+ }
const gitOptions: GitOptions = {
owner: host.repository.owner,
@@ -32,8 +32,7 @@ export const useStudio = createSharedComposable(() => {
authorEmail: host.user.get().email,
}
- const git = studioFlags.dev ? useDevelopmentGit(gitOptions) : useGit(gitOptions)
- const { preferences, setManifestId } = useStudioState()
+ const git = devMode.value ? useDevelopmentGit(gitOptions) : useGit(gitOptions)
const ui = useUI(host)
const draftDocuments = useDraftDocuments(host, git)
const documentTree = useTree(StudioFeature.Content, host, draftDocuments)
@@ -42,8 +41,8 @@ export const useStudio = createSharedComposable(() => {
const context = useContext(host, git, documentTree, mediaTree)
host.on.mounted(async () => {
- if (studioFlags.dev) {
- initDevelopmentMode(host, documentTree, mediaTree)
+ if (devMode.value) {
+ initDevelopmentMode()
}
await draftDocuments.load()
@@ -80,7 +79,8 @@ export const useStudio = createSharedComposable(() => {
}
})
-function initDevelopmentMode(host: StudioHost, documentTree: ReturnType, mediaTree: ReturnType) {
+function initDevelopmentMode() {
+ const { host, documentTree, mediaTree, context, ui } = useStudio()
const hooks = useHooks()
// Disable browser storages
@@ -129,4 +129,13 @@ function initDevelopmentMode(host: StudioHost, documentTree: ReturnType {
+ if (context.currentFeature.value !== StudioFeature.Content) {
+ context.switchFeature(StudioFeature.Content)
+ }
+
+ documentTree.selectItemByFsPath(fsPath)
+ ui.open()
+ })
}
diff --git a/src/app/src/composables/useStudioState.ts b/src/app/src/composables/useStudioState.ts
index 624efb37..91643b3a 100644
--- a/src/app/src/composables/useStudioState.ts
+++ b/src/app/src/composables/useStudioState.ts
@@ -4,6 +4,7 @@ import type { StudioConfig, StudioLocation } from '../types'
import { StudioFeature } from '../types/context'
export const useStudioState = createSharedComposable(() => {
+ const devMode = ref(false)
const manifestId = ref('')
const preferences = useStorage('studio-preferences', { syncEditorAndRoute: true, showTechnicalMode: false })
const location = useStorage('studio-active', { active: false, feature: StudioFeature.Content, fsPath: '/' })
@@ -20,14 +21,20 @@ export const useStudioState = createSharedComposable(() => {
manifestId.value = id
}
+ function enableDevMode() {
+ devMode.value = true
+ }
+
function updatePreference(key: K, value: StudioConfig[K]) {
preferences.value = { ...preferences.value, [key]: value }
}
return {
+ devMode: readonly(devMode),
manifestId: readonly(manifestId),
preferences: readonly(preferences),
location: readonly(location),
+ enableDevMode,
setLocation,
unsetActiveLocation,
setManifestId,
diff --git a/src/app/src/composables/useTree.ts b/src/app/src/composables/useTree.ts
index 99071a75..67d35959 100644
--- a/src/app/src/composables/useTree.ts
+++ b/src/app/src/composables/useTree.ts
@@ -10,7 +10,7 @@ import { useStudioState } from './useStudioState'
export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType) => {
const hooks = useHooks()
- const { preferences, setLocation } = useStudioState()
+ const { preferences, setLocation, devMode } = useStudioState()
const tree = ref([])
@@ -101,7 +101,7 @@ export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType
const hostDb = type === StudioFeature.Content ? host.document.db : host.media
const list = await hostDb.list() as DatabaseItem[]
- tree.value = buildTree(list, draft.list.value)
+ tree.value = buildTree(list, draft.list.value, devMode.value)
// Reselect current item to update status
if (selectItem) {
diff --git a/src/app/src/types/index.ts b/src/app/src/types/index.ts
index a2a46ec8..155871f8 100644
--- a/src/app/src/types/index.ts
+++ b/src/app/src/types/index.ts
@@ -30,6 +30,7 @@ export interface StudioHost {
manifestUpdate: (fn: (id: string) => void) => void
documentUpdate: (fn: (fsPath: string, type: 'remove' | 'update') => void) => void
mediaUpdate: (fn: (fsPath: string, type: 'remove' | 'update') => void) => void
+ requestDocumentEdit: (fn: (fsPath: string) => void) => void
}
ui: {
colorMode: 'light' | 'dark'
diff --git a/src/app/src/utils/tree.ts b/src/app/src/utils/tree.ts
index 61aa7b91..e99d1a3c 100644
--- a/src/app/src/utils/tree.ts
+++ b/src/app/src/utils/tree.ts
@@ -6,7 +6,6 @@ import {
} from '../types'
import type { RouteLocationNormalized } from 'vue-router'
import type { BaseItem } from '../types/item'
-import { studioFlags } from '../composables/useStudio'
import { getFileExtension, parseName } from './file'
export const COLOR_STATUS_MAP: { [key in TreeStatus]?: string } = {
@@ -25,7 +24,7 @@ export const COLOR_UI_STATUS_MAP: { [key in TreeStatus]?: string } = {
[TreeStatus.Opened]: 'neutral',
} as const
-export function buildTree(dbItems: BaseItem[], draftList: DraftItem[] | null):
+export function buildTree(dbItems: BaseItem[], draftList: DraftItem[] | null, isDev = false):
TreeItem[] {
const tree: TreeItem[] = []
const directoryMap = new Map()
@@ -156,7 +155,7 @@ TreeItem[] {
directoryChildren.push(fileItem)
}
- calculateDirectoryStatuses(tree)
+ calculateDirectoryStatuses(tree, isDev)
return tree
}
@@ -283,8 +282,8 @@ export function findDescendantsFileItemsFromFsPath(tree: TreeItem[], fsPath: str
return descendants
}
-function calculateDirectoryStatuses(items: TreeItem[]) {
- if (studioFlags.dev) {
+function calculateDirectoryStatuses(items: TreeItem[], isDev = false) {
+ if (isDev) {
return
}
@@ -293,7 +292,7 @@ function calculateDirectoryStatuses(items: TreeItem[]) {
continue
}
- calculateDirectoryStatuses(item.children)
+ calculateDirectoryStatuses(item.children, isDev)
const childrenWithStatus = item.children.filter(child => child.status && child.status !== TreeStatus.Opened)
diff --git a/src/module/src/module.ts b/src/module/src/module.ts
index cbe7fb4f..2af8456b 100644
--- a/src/module/src/module.ts
+++ b/src/module/src/module.ts
@@ -71,8 +71,12 @@ interface ModuleOptions {
}
/**
* Enable Nuxt Studio to edit content and media files on your filesystem.
- * Currently experimental.
- * @experimental
+ */
+ dev: boolean
+ /**
+ * Enable Nuxt Studio to edit content and media files on your filesystem.
+ *
+ * @deprecated Use the 'dev' option instead.
*/
development?: {
sync?: boolean
@@ -103,16 +107,14 @@ export default defineNuxtModule({
clientSecret: process.env.STUDIO_GITHUB_CLIENT_SECRET,
},
},
- development: {
- sync: false,
- },
+ dev: true,
},
async setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
const runtime = (...args: string[]) => resolver.resolve('./runtime', ...args)
- if (!nuxt.options.dev) {
- options.development!.sync = false
+ if (nuxt.options.dev === false || options.development?.sync === false) {
+ options.dev = false
}
if (!nuxt.options.dev && !nuxt.options._prepare) {
@@ -130,8 +132,8 @@ export default defineNuxtModule({
nuxt.options.runtimeConfig.public.studio = {
route: options.route!,
+ dev: Boolean(options.dev),
development: {
- sync: Boolean(options.development!.sync),
server: process.env.STUDIO_DEV_SERVER,
},
// @ts-expect-error Autogenerated type does not match with options
@@ -176,7 +178,7 @@ export default defineNuxtModule({
]
})
- if (options.development!.sync) {
+ if (options.dev) {
nuxt.options.nitro.storage = {
...nuxt.options.nitro.storage,
nuxt_studio_content: {
@@ -185,7 +187,7 @@ export default defineNuxtModule({
},
nuxt_studio_public_assets: {
driver: 'fs',
- base: resolve(nuxt.options.srcDir, 'public'),
+ base: resolve(nuxt.options.rootDir, 'public'),
},
}
addServerHandler({
@@ -220,7 +222,7 @@ export default defineNuxtModule({
})
addTemplate({
filename: 'studio-public-assets.mjs',
- getContents: () => options.development!.sync
+ getContents: () => options.dev
? getAssetsStorageDevTemplate(assetsStorage, nuxt)
: getAssetsStorageTemplate(assetsStorage, nuxt),
})
diff --git a/src/module/src/runtime/host.dev.ts b/src/module/src/runtime/host.dev.ts
index 6e5a41fc..13a5d6e2 100644
--- a/src/module/src/runtime/host.dev.ts
+++ b/src/module/src/runtime/host.dev.ts
@@ -12,7 +12,7 @@ import { getCollectionSourceById } from './utils/source'
export function useStudioHost(user: StudioUser, repository: Repository) {
const host = useStudioHostBase(user, repository)
- if (!useRuntimeConfig().public.studio.development.sync) {
+ if (!useRuntimeConfig().public.studio.dev) {
return host
}
diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts
index 0a64d85e..3b6c70fc 100644
--- a/src/module/src/runtime/host.ts
+++ b/src/module/src/runtime/host.ts
@@ -142,6 +142,10 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
mediaUpdate: (_fn: (id: string, type: 'remove' | 'update') => void) => {
// no operation
},
+ requestDocumentEdit: (fn: (fsPath: string) => void) => {
+ // @ts-expect-error studio:document:edit is not defined in types
+ useNuxtApp().hooks.hook('studio:document:edit', fn)
+ },
},
ui: {
colorMode,
@@ -347,6 +351,27 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
}
return meta.fetch()
})
+
+ document.body.addEventListener('dblclick', (event: MouseEvent) => {
+ let element = event.target as HTMLElement
+ while (element) {
+ if (element.getAttribute('data-content-id')) {
+ break
+ }
+ element = element.parentElement as HTMLElement
+ }
+ if (element) {
+ const id = element.getAttribute('data-content-id')!
+ const collection = getCollectionById(id, useContentCollections())
+ const source = getCollectionSourceById(id, collection.source)
+ const fsPath = generateFsPathFromId(id, source!)
+
+ // @ts-expect-error studio:document:edit is not defined in types
+ useNuxtApp().hooks.callHook('studio:document:edit', fsPath)
+ }
+ })
+ // Initialize styles
+ host.ui.updateStyles()
})()
return host
diff --git a/src/module/src/runtime/server/routes/dev/public/[...path].ts b/src/module/src/runtime/server/routes/dev/public/[...path].ts
index 8b7066fe..b224da49 100644
--- a/src/module/src/runtime/server/routes/dev/public/[...path].ts
+++ b/src/module/src/runtime/server/routes/dev/public/[...path].ts
@@ -1,6 +1,7 @@
import type { H3Event } from 'h3'
import { createError, eventHandler, getRequestHeader, readRawBody, setResponseHeader } from 'h3'
import type { Storage, StorageMeta } from 'unstorage'
+import { withLeadingSlash } from 'ufo'
// @ts-expect-error useStorage is not defined in .nuxt/imports.d.ts
import { useStorage } from '#imports'
@@ -30,6 +31,7 @@ export default eventHandler(async (event) => {
extension: key.split('.').pop(),
stem: key.split('.').join('.'),
path: '/' + key.replace(/:/g, '/'),
+ fsPath: withLeadingSlash(key.replace(/:/g, '/')),
version: new Date(item.mtime || new Date()).getTime(),
}
}
diff --git a/src/module/src/runtime/utils/activation.ts b/src/module/src/runtime/utils/activation.ts
index 71d4aead..274c10a4 100644
--- a/src/module/src/runtime/utils/activation.ts
+++ b/src/module/src/runtime/utils/activation.ts
@@ -5,6 +5,17 @@ export async function defineStudioActivationPlugin(onStudioActivation: (user: St
const user = useState('studio-session', () => null)
const config = useRuntimeConfig().public.studio
+ if (config.dev) {
+ return await onStudioActivation({
+ provider: 'github',
+ email: 'dev@nuxt.com',
+ name: 'Dev',
+ githubToken: '',
+ githubId: '',
+ avatar: '',
+ })
+ }
+
await $fetch<{ user: StudioUser }>('/__nuxt_studio/auth/session').then((session) => {
user.value = session?.user ?? null
})