Saltar al contenido

Firebase y Astro

Firebase es una plataforma de desarrollo de aplicaciones que proporciona una base de datos NoSQL, autenticación, suscripciones en tiempo real, funciones y almacenamiento.

Consulta nuestra guía aparte para desplegar en el hosting de Firebase.

  • Un proyecto de Firebase con una aplicación web configurada.
  • Un proyecto de Astro con output: 'server' habilitado para renderizado bajo demanda.
  • Firebase credentials: You will need two sets of credentials to connect Astro to Firebase:
    • Credenciales de la aplicación web: Estas credenciales se usarán en el lado del cliente de tu aplicación. Puedes encontrarlas en la consola de Firebase en Configuración del proyecto > General. Desplázate hacia abajo hasta la sección Tus aplicaciones y haz clic en el icono de Aplicación web.
    • Credenciales del proyecto: Estas credenciales se usarán en el lado del servidor de tu aplicación. Puedes generarlas en la consola de Firebase en Configuración del proyecto > Cuentas de servicio > SDK de Firebase Admin > Generar nueva clave privada.

Para añadir tus credenciales de Firebase a Astro, crea un archivo .env en la raíz de tu proyecto con las siguientes variables:

.env
FIREBASE_PRIVATE_KEY_ID=YOUR_PRIVATE_KEY_ID
FIREBASE_PRIVATE_KEY=YOUR_PRIVATE_KEY
FIREBASE_PROJECT_ID=YOUR_PROJECT_ID
FIREBASE_CLIENT_EMAIL=YOUR_CLIENT_EMAIL
FIREBASE_CLIENT_ID=YOUR_CLIENT_ID
FIREBASE_AUTH_URI=YOUR_AUTH_URI
FIREBASE_TOKEN_URI=YOUR_TOKEN_URI
FIREBASE_AUTH_CERT_URL=YOUR_AUTH_CERT_URL
FIREBASE_CLIENT_CERT_URL=YOUR_CLIENT_CERT_URL

Ahora, estas variables de entorno están disponibles para su uso en tu proyecto.

Si deseas tener IntelliSense para tus variables de entorno de Firebase, edita o crea el archivo env.d.ts en tu directorio src/ y configura tus tipos:

src/env.d.ts
interface ImportMetaEnv {
readonly FIREBASE_PRIVATE_KEY_ID: string;
readonly FIREBASE_PRIVATE_KEY: string;
readonly FIREBASE_PROJECT_ID: string;
readonly FIREBASE_CLIENT_EMAIL: string;
readonly FIREBASE_CLIENT_ID: string;
readonly FIREBASE_AUTH_URI: string;
readonly FIREBASE_TOKEN_URI: string;
readonly FIREBASE_AUTH_CERT_URL: string
readonly FIREBASE_CLIENT_CERT_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

Tu proyecto debería incluir ahora estos nuevos archivos:

  • Directoriosrc/
    • env.d.ts
  • .env
  • astro.config.mjs
  • package.json

Para conectar Astro con Firebase, instala los siguientes paquetes usando el comando único a continuación para tu gestor de paquetes preferido:

  • firebase: el SDK de Firebase para el lado del cliente
  • firebase-admin: el SDK de Firebase Admin para el lado del servidor
Ventana de la terminal
npm install firebase firebase-admin

A continuación, crea una carpeta llamada firebase en el directorio src/ y añade dos nuevos archivos a esta carpeta: client.ts y server.ts.

En client.ts, añade el siguiente código para inicializar Firebase en el cliente usando las credenciales de tu aplicación web y el paquete firebase:

src/firebase/client.ts
import { initializeApp } from "firebase/app";
const firebaseConfig = {
apiKey: "my-public-api-key",
authDomain: "my-auth-domain",
projectId: "my-project-id",
storageBucket: "my-storage-bucket",
messagingSenderId: "my-sender-id",
appId: "my-app-id",
};
export const app = initializeApp(firebaseConfig);

En server.ts, añade el siguiente código para inicializar Firebase en el servidor usando las credenciales de tu proyecto y el paquete firebase-admin:

src/firebase/server.ts
import type { ServiceAccount } from "firebase-admin";
import { initializeApp, cert, getApps } from "firebase-admin/app";
const activeApps = getApps();
const serviceAccount = {
type: "service_account",
project_id: import.meta.env.FIREBASE_PROJECT_ID,
private_key_id: import.meta.env.FIREBASE_PRIVATE_KEY_ID,
private_key: import.meta.env.FIREBASE_PRIVATE_KEY,
client_email: import.meta.env.FIREBASE_CLIENT_EMAIL,
client_id: import.meta.env.FIREBASE_CLIENT_ID,
auth_uri: import.meta.env.FIREBASE_AUTH_URI,
token_uri: import.meta.env.FIREBASE_TOKEN_URI,
auth_provider_x509_cert_url: import.meta.env.FIREBASE_AUTH_CERT_URL,
client_x509_cert_url: import.meta.env.FIREBASE_CLIENT_CERT_URL,
};
const initApp = () => {
if (import.meta.env.PROD) {
console.info('PROD env detected. Using default service account.')
// Use default config in firebase functions. Should be already injected in the server by Firebase.
return initializeApp()
}
console.info('Loading service account from env.')
return initializeApp({
credential: cert(serviceAccount as ServiceAccount)
})
}
export const app = activeApps.length === 0 ? initApp() : activeApps[0];

Finalmente, tu proyecto debería incluir ahora estos nuevos archivos:

  • Directoriosrc
    • env.d.ts
    • Directoriofirebase
      • client.ts
      • server.ts
  • .env
  • astro.config.mjs
  • package.json
  • Un proyecto de Astro inicializado con Firebase.
  • Un proyecto de Firebase con autenticación de correo/contraseña habilitada en la consola de Firebase bajo Autenticación > Método de inicio de sesión.

La autenticación de Firebase en Astro requiere los siguientes tres endpoints de servidor de Astro:

  • GET /api/auth/signin - para iniciar la sesión de un usuario
  • GET /api/auth/signout - para cerrar la sesión de un usuario
  • POST /api/auth/register - para registrar a un usuario

Crea tres endpoints relacionados con la autenticación en un nuevo directorio src/pages/api/auth/: signin.ts, signout.ts y register.ts.

signin.ts contiene el código para iniciar la sesión de un usuario usando Firebase:

src/pages/api/auth/signin.ts
import type { APIRoute } from "astro";
import { app } from "../../../firebase/server";
import { getAuth } from "firebase-admin/auth";
export const GET: APIRoute = async ({ request, cookies, redirect }) => {
const auth = getAuth(app);
/* Get token from request headers */
const idToken = request.headers.get("Authorization")?.split("Bearer ")[1];
if (!idToken) {
return new Response(
"No token found",
{ status: 401 }
);
}
/* Verify id token */
try {
await auth.verifyIdToken(idToken);
} catch (error) {
return new Response(
"Invalid token",
{ status: 401 }
);
}
/* Create and set session cookie */
const fiveDays = 60 * 60 * 24 * 5 * 1000;
const sessionCookie = await auth.createSessionCookie(idToken, {
expiresIn: fiveDays,
});
cookies.set("__session", sessionCookie, {
path: "/",
});
return redirect("/dashboard");
};

signout.ts contiene el código para cerrar la sesión de un usuario eliminando la cookie de sesión:

src/pages/api/auth/signout.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = async ({ redirect, cookies }) => {
cookies.delete("__session", {
path: "/",
});
return redirect("/signin");
};

register.ts contiene el código para registrar a un usuario usando Firebase:

src/pages/api/auth/register.ts
import type { APIRoute } from "astro";
import { getAuth } from "firebase-admin/auth";
import { app } from "../../../firebase/server";
export const POST: APIRoute = async ({ request, redirect }) => {
const auth = getAuth(app);
/* Get form data */
const formData = await request.formData();
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
const name = formData.get("name")?.toString();
if (!email || !password || !name) {
return new Response(
"Missing form data",
{ status: 400 }
);
}
/* Create user */
try {
await auth.createUser({
email,
password,
displayName: name,
});
} catch (error: any) {
return new Response(
"Something went wrong",
{ status: 400 }
);
}
return redirect("/signin");
};

Después de crear los endpoints de servidor para la autenticación, el directorio de tu proyecto debería incluir ahora estos nuevos archivos:

  • Directoriosrc
    • env.d.ts
    • Directoriofirebase
      • client.ts
      • server.ts
    • Directoriopages
      • Directorioapi
        • Directorioauth
          • signin.ts
          • signout.ts
          • register.ts
  • .env
  • astro.config.mjs
  • package.json

Crea las páginas que usarán los endpoints de Firebase:

  • src/pages/register - contendrá un formulario para registrar a un usuario
  • src/pages/signin - contendrá un formulario para iniciar la sesión de un usuario
  • src/pages/dashboard - contendrá un panel de control al que solo pueden acceder los usuarios autenticados

El ejemplo de src/pages/register.astro a continuación incluye un formulario que enviará una solicitud POST al endpoint /api/auth/register. Este endpoint creará un nuevo usuario utilizando los datos del formulario y luego redirigirá al usuario a la página /signin.

src/pages/register.astro
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="Register">
<h1>Register</h1>
<p>Already have an account? <a href="/signin">Sign in</a></p>
<form action="/api/auth/register" method="post">
<label for="name">Name</label>
<input type="text" name="name" id="name" />
<label for="email" for="email">Email</label>
<input type="email" name="email" id="email" />
<label for="password">Password</label>
<input type="password" name="password" id="password" />
<button type="submit">Login</button>
</form>
</Layout>

src/pages/signin.astro utiliza la aplicación de servidor de Firebase para verificar la cookie de sesión del usuario. Si el usuario está autenticado, la página lo redirigirá a la página /dashboard.

La página de ejemplo a continuación contiene un formulario que enviará una solicitud POST al endpoint /api/auth/signin con el token de ID generado por la aplicación cliente de Firebase.

El endpoint verificará el token de ID y creará una nueva cookie de sesión para el usuario. Luego, el endpoint redirigirá al usuario a la página /dashboard.

src/pages/signin.astro
---
import { app } from "../firebase/server";
import { getAuth } from "firebase-admin/auth";
import Layout from "../layouts/Layout.astro";
/* Check if the user is authenticated */
const auth = getAuth(app);
if (Astro.cookies.has("__session")) {
const sessionCookie = Astro.cookies.get("__session")!.value;
const decodedCookie = await auth.verifySessionCookie(sessionCookie);
if (decodedCookie) {
return Astro.redirect("/dashboard");
}
}
---
<Layout title="Sign in">
<h1>Sign in</h1>
<p>New here? <a href="/register">Create an account</a></p>
<form action="/api/auth/signin" method="post">
<label for="email" for="email">Email</label>
<input type="email" name="email" id="email" />
<label for="password">Password</label>
<input type="password" name="password" id="password" />
<button type="submit">Login</button>
</form>
</Layout>
<script>
import {
getAuth,
inMemoryPersistence,
signInWithEmailAndPassword,
} from "firebase/auth";
import { app } from "../firebase/client";
const auth = getAuth(app);
// This will prevent the browser from storing session data
auth.setPersistence(inMemoryPersistence);
const form = document.querySelector("form") as HTMLFormElement;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
if (!email || !password) {
return;
}
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
const idToken = await userCredential.user.getIdToken();
const response = await fetch("/api/auth/signin", {
method: "GET",
headers: {
Authorization: `Bearer ${idToken}`,
},
});
if (response.redirected) {
window.location.assign(response.url);
}
});
</script>

src/pages/dashboard.astro verificará la cookie de sesión del usuario utilizando la aplicación de servidor de Firebase. Si el usuario no está autenticado, la página lo redirigirá a la página /signin.

La página de ejemplo a continuación muestra el nombre del usuario y un botón para cerrar sesión. Al hacer clic en el botón, se enviará una solicitud GET al endpoint /api/auth/signout.

El endpoint eliminará la cookie de sesión del usuario y lo redirigirá a la página /signin.

src/pages/dashboard.astro
---
import { app } from "../firebase/server";
import { getAuth } from "firebase-admin/auth";
import Layout from "../layouts/Layout.astro";
const auth = getAuth(app);
/* Check current session */
if (!Astro.cookies.has("__session")) {
return Astro.redirect("/signin");
}
const sessionCookie = Astro.cookies.get("__session")!.value;
const decodedCookie = await auth.verifySessionCookie(sessionCookie);
const user = await auth.getUser(decodedCookie.uid);
if (!user) {
return Astro.redirect("/signin");
}
---
<Layout title="dashboard">
<h1>Welcome {user.displayName}</h1>
<p>We are happy to see you here</p>
<form action="/api/auth/signout">
<button type="submit">Sign out</button>
</form>
</Layout>

Para añadir proveedores de OAuth a tu aplicación, debes habilitarlos en la consola de Firebase.

En la consola de Firebase, ve a la sección Autenticación y haz clic en la pestaña Método de inicio de sesión. Luego, haz clic en el botón Añadir nuevo proveedor y habilita los proveedores que quieras usar.

El ejemplo a continuación utiliza el proveedor de Google.

Edita la página signin.astro para añadir:

  • un botón para iniciar sesión con Google debajo del formulario existente
  • un detector de eventos en el botón para manejar el proceso de inicio de sesión en la etiqueta <script> existente.
src/pages/signin.astro
---
import { app } from "../firebase/server";
import { getAuth } from "firebase-admin/auth";
import Layout from "../layouts/Layout.astro";
/* Check if the user is authenticated */
const auth = getAuth(app);
if (Astro.cookies.has("__session")) {
const sessionCookie = Astro.cookies.get("__session")!.value;
const decodedCookie = await auth.verifySessionCookie(sessionCookie);
if (decodedCookie) {
return Astro.redirect("/dashboard");
}
}
---
<Layout title="Sign in">
<h1>Sign in</h1>
<p>New here? <a href="/register">Create an account</a></p>
<form action="/api/auth/signin" method="post">
<label for="email" for="email">Email</label>
<input type="email" name="email" id="email" />
<label for="password">Password</label>
<input type="password" name="password" id="password" />
<button type="submit">Login</button>
</form>
<button id="google">Sign in with Google</button>
</Layout>
<script>
import {
getAuth,
inMemoryPersistence,
signInWithEmailAndPassword,
GoogleAuthProvider,
signInWithPopup,
} from "firebase/auth";
import { app } from "../firebase/client";
const auth = getAuth(app);
auth.setPersistence(inMemoryPersistence);
const form = document.querySelector("form") as HTMLFormElement;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
if (!email || !password) {
return;
}
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
const idToken = await userCredential.user.getIdToken();
const response = await fetch("/api/auth/signin", {
headers: {
Authorization: `Bearer ${idToken}`,
},
});
if (response.redirected) {
window.location.assign(response.url);
}
});
const googleSignin = document.querySelector("#google") as HTMLButtonElement;
googleSignin.addEventListener("click", async () => {
const provider = new GoogleAuthProvider();
const userCredential = await signInWithPopup(auth, provider);
const idToken = await userCredential.user.getIdToken();
const res = await fetch("/api/auth/signin", {
headers: {
Authorization: `Bearer ${idToken}`,
},
});
if (res.redirected) {
window.location.assign(res.url);
}
});
</script>

Cuando se haga clic, el botón de inicio de sesión de Google abrirá una ventana emergente para iniciar sesión con Google. Una vez que el usuario inicie sesión, enviará una solicitud POST al endpoint /api/auth/signin con el token de ID generado por el proveedor de OAuth.

El endpoint verificará el token de ID y creará una nueva cookie de sesión para el usuario. Luego, el endpoint redirigirá al usuario a la página /dashboard.

En esta receta, la colección de Firestore se llamará friends y contendrá documentos con los siguientes campos:

  • id: autogenerado por Firestore
  • name: un campo de texto (string)
  • age: un campo numérico (number)
  • isBestFriend: un campo booleano (boolean)

Crea dos nuevos archivos en un nuevo directorio src/pages/api/friends/: index.ts y [id].ts. Estos crearán dos endpoints de servidor para interactuar con la base de datos Firestore de las siguientes maneras:

  • POST /api/friends: para crear un nuevo documento en la colección friends.
  • POST /api/friends/:id: para actualizar un documento en la colección friends.
  • DELETE /api/friends/:id: para eliminar un documento en la colección friends.

index.ts contendrá el código para crear un nuevo documento en la colección friends:

src/pages/api/friends/index.ts
import type { APIRoute } from "astro";
import { app } from "../../../firebase/server";
import { getFirestore } from "firebase-admin/firestore";
export const POST: APIRoute = async ({ request, redirect }) => {
const formData = await request.formData();
const name = formData.get("name")?.toString();
const age = formData.get("age")?.toString();
const isBestFriend = formData.get("isBestFriend") === "on";
if (!name || !age) {
return new Response("Missing required fields", {
status: 400,
});
}
try {
const db = getFirestore(app);
const friendsRef = db.collection("friends");
await friendsRef.add({
name,
age: parseInt(age),
isBestFriend,
});
} catch (error) {
return new Response("Something went wrong", {
status: 500,
});
}
return redirect("/dashboard");
};

[id].ts contendrá el código para actualizar y eliminar un documento en la colección friends:

src/pages/api/friends/[id].ts
import type { APIRoute } from "astro";
import { app } from "../../../firebase/server";
import { getFirestore } from "firebase-admin/firestore";
const db = getFirestore(app);
const friendsRef = db.collection("friends");
export const POST: APIRoute = async ({ params, redirect, request }) => {
const formData = await request.formData();
const name = formData.get("name")?.toString();
const age = formData.get("age")?.toString();
const isBestFriend = formData.get("isBestFriend") === "on";
if (!name || !age) {
return new Response("Missing required fields", {
status: 400,
});
}
if (!params.id) {
return new Response("Cannot find friend", {
status: 404,
});
}
try {
await friendsRef.doc(params.id).update({
name,
age: parseInt(age),
isBestFriend,
});
} catch (error) {
return new Response("Something went wrong", {
status: 500,
});
}
return redirect("/dashboard");
};
export const DELETE: APIRoute = async ({ params, redirect }) => {
if (!params.id) {
return new Response("Cannot find friend", {
status: 404,
});
}
try {
await friendsRef.doc(params.id).delete();
} catch (error) {
return new Response("Something went wrong", {
status: 500,
});
}
return redirect("/dashboard");
};

Después de crear los endpoints de servidor para Firestore, el directorio de tu proyecto debería incluir ahora estos nuevos archivos:

  • Directoriosrc
    • env.d.ts
    • Directoriofirebase
      • client.ts
      • server.ts
    • Directoriopages
      • Directorioapi
        • Directoriofriends
          • index.ts
          • [id].ts
  • .env
  • astro.config.mjs
  • package.json

Crea las páginas que usarán los endpoints de Firestore:

  • src/pages/add.astro - contendrá un formulario para añadir un nuevo amigo.
  • src/pages/edit/[id].astro - contendrá un formulario para editar un amigo y un botón para eliminar un amigo.
  • src/pages/friend/[id].astro - contendrá los detalles de un amigo.
  • src/pages/dashboard.astro - mostrará una lista de amigos.

El ejemplo de src/pages/add.astro a continuación incluye un formulario que enviará una solicitud POST al endpoint /api/friends. Este endpoint creará un nuevo amigo utilizando los datos del formulario y luego redirigirá al usuario a la página /dashboard.

src/pages/add.astro
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="Add a new friend">
<h1>Add a new friend</h1>
<form method="post" action="/api/friends">
<label for="name">Name</label>
<input type="text" id="name" name="name" />
<label for="age">Age</label>
<input type="number" id="age" name="age" />
<label for="isBestFriend">Is best friend?</label>
<input type="checkbox" id="isBestFriend" name="isBestFriend" />
<button type="submit">Add friend</button>
</form>
</Layout>

src/pages/edit/[id].astro contendrá un formulario para editar los datos de un amigo y un botón para eliminar un amigo. Al enviar el formulario, esta página enviará una solicitud POST al endpoint /api/friends/:id para actualizar los datos del amigo.

Si el usuario hace clic en el botón de eliminar, esta página enviará una solicitud DELETE al endpoint /api/friends/:id para eliminar al amigo.

src/pages/edit/[id].astro
---
import Layout from "../../layouts/Layout.astro";
import { app } from "../../firebase/server";
import { getFirestore } from "firebase-admin/firestore";
interface Friend {
name: string;
age: number;
isBestFriend: boolean;
}
const { id } = Astro.params;
if (!id) {
return Astro.redirect("/404");
}
const db = getFirestore(app);
const friendsRef = db.collection("friends");
const friendSnapshot = await friendsRef.doc(id).get();
if (!friendSnapshot.exists) {
return Astro.redirect("/404");
}
const friend = friendSnapshot.data() as Friend;
---
<Layout title="Edit {friend.name}">
<h1>Edit {friend.name}</h1>
<p>Here you can edit or delete your friend's data.</p>
<form method="post" action={`/api/friends/${id}`}>
<label for="name">Name</label>
<input type="text" id="name" name="name" value={friend.name} />
<label for="age">Age</label>
<input type="number" id="age" name="age" value={friend.age} />
<label for="isBestFriend">Is best friend?</label>
<input
type="checkbox"
id="isBestFriend"
name="isBestFriend"
checked={friend.isBestFriend}
/>
<button type="submit">Edit friend</button>
</form>
<button type="button" id="delete-document">Delete</button>
</Layout>
<script>
const deleteButton = document.getElementById(
"delete-document"
) as HTMLButtonElement;
const url = document.querySelector("form")?.getAttribute("action") as string;
deleteButton.addEventListener("click", async () => {
const response = await fetch(url, {
method: "DELETE",
});
if (response.redirected) {
window.location.assign(response.url);
}
});
</script>

src/pages/friend/[id].astro mostrará los detalles de un amigo.

src/pages/friend/[id].astro
---
import Layout from "../../layouts/Layout.astro";
import { app } from "../../firebase/server";
import { getFirestore } from "firebase-admin/firestore";
interface Friend {
name: string;
age: number;
isBestFriend: boolean;
}
const { id } = Astro.params;
if (!id) {
return Astro.redirect("/404");
}
const db = getFirestore(app);
const friendsRef = db.collection("friends");
const friendSnapshot = await friendsRef.doc(id).get();
if (!friendSnapshot.exists) {
return Astro.redirect("/404");
}
const friend = friendSnapshot.data() as Friend;
---
<Layout title={friend.name}>
<h1>{friend.name}</h1>
<p>Age: {friend.age}</p>
<p>Is best friend: {friend.isBestFriend ? "Yes" : "No"}</p>
</Layout>

Mostrar una lista de registros con un botón de editar

Sección titulada “Mostrar una lista de registros con un botón de editar”

Finalmente, src/pages/dashboard.astro mostrará una lista de amigos. Cada amigo tendrá un enlace a su página de detalles y un botón de editar que redirigirá al usuario a la página de edición.

src/pages/dashboard.astro
---
import { app } from "../firebase/server";
import { getFirestore } from "firebase-admin/firestore";
import Layout from "../layouts/Layout.astro";
interface Friend {
id: string;
name: string;
age: number;
isBestFriend: boolean;
}
const db = getFirestore(app);
const friendsRef = db.collection("friends");
const friendsSnapshot = await friendsRef.get();
const friends = friendsSnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Friend[];
---
<Layout title="My friends">
<h1>Friends</h1>
<ul>
{
friends.map((friend) => (
<li>
<a href={`/friend/${friend.id}`}>{friend.name}</a>
<span>({friend.age})</span>
<strong>{friend.isBestFriend ? "Bestie" : "Friend"}</strong>
<a href={`/edit/${friend.id}`}>Edit</a>
</li>
))
}
</ul>
</Layout>

Después de crear todas las páginas, deberías tener la siguiente estructura de archivos:

  • Directoriosrc
    • env.d.ts
    • Directoriofirebase
      • client.ts
      • server.ts
    • Directoriopages
      • dashboard.astro
      • add.astro
      • Directorioedit
        • [id].astro
      • Directoriofriend
        • [id].astro
      • Directorioapi
        • Directoriofriends
          • index.ts
          • [id].ts
  • .env
  • astro.config.mjs
  • package.json

Más guías de servicios de backend

Contribuir Comunidad Patrocinar