Saltearse al contenido

Colecciones de Contenido

Agregado en: astro@2.0.0

Las Colecciones de contenido son la mejor manera de administrar y crear contenido en cualquier proyecto de Astro. Las colecciones ayudan a organizar tus documentos, validar tu frontmatter y proporcionar una seguridad de tipo automática de TypeScript para todo tu contenido.

¿Qué son las Colecciones de Contenido?

Sección titulada ¿Qué son las Colecciones de Contenido?

Una colección de contenido es cualquier directorio de nivel superior dentro del directorio reservado src/content del proyecto, como src/content/newsletter y src/content/authors. Solo se permiten colecciones de contenido dentro del directorio src/content. Este directorio no se puede utilizar para nada más.

Una entrada de colección es cualquier pieza de contenido dentro de tu directorio de colecciones de contenido. Las entradas pueden usar formatos de autoría de contenido que incluyen Markdown (.md) y MDX (.mdx usando la integración MDX) o como uno de los dos formatos de datos admitidos: YAML (.yaml) y JSON (.json). Recomendamos usar un esquema de nomenclatura consistente (minúsculas, guiones en lugar de espacios) para tus archivos para facilitar la búsqueda y organización de tu contenido, pero esto no es obligatorio. También puedes excluir las entradas de ser construidas prefijando el nombre de archivo con un guión bajo (_).

  • Directorysrc/content/
    • Directorynewsletter/ la colección “newsletter”
      • week-1.md una entrada de contenido
      • week-2.md una entrada de contenido
      • week-3.md una entrada de contenido

Una vez que tengas una colección, puedes comenzar a consultar tu contenido utilizando las APIs de contenido integradas de Astro.

Astro guarda metadatos importantes para las colecciones de contenido en un directorio .astro en tu proyecto. No es necesario que realices ninguna acción para mantener o actualizar este directorio. Se te recomienda que lo ignores por completo mientras trabajas en tu proyecto.

El directorio .astro se actualizará automáticamente cada vez que ejecutes los comandos astro dev, astro build. Puedes ejecutar astro sync en cualquier momento para actualizar el directorio .astro manualmente.

Organizando con múltiples colecciones

Sección titulada Organizando con múltiples colecciones

Si dos archivos representan diferentes tipos de contenido (por ejemplo, una publicación de blog y un perfil de autor), probablemente pertenezcan a diferentes colecciones. Esto es importante porque muchas características (validación de frontmatter, seguridad de tipo automático de TypeScript) requieren que todas las entradas de una colección compartan una estructura similar.

Si puedes trabajar con diferentes tipos de contenido, debes crear múltiples colecciones para representar cada tipo. Puedes crear tantas colecciones diferentes en tu proyecto como desees.

  • Directorysrc/content/
    • Directorynewsletter/
      • week-1.md
      • week-2.md
    • Directoryblog/
      • post-1.md
      • post-2.md
    • Directoryauthors/
      • grace-hopper.json
      • alan-turing.json

Una colección de contenido siempre es una carpeta de nivel superior dentro del directorio src/content/. No puedes anidar una colección dentro de otra. Sin embargo, puedes usar subdirectorios para organizar tu contenido dentro de una colección.

Por ejemplo, puedes usar la siguiente estructura de directorios para organizar las traducciones de i18n dentro de una sola colección docs. Cuando consultes esta colección, podrás filtrar el resultado por idioma utilizando la ruta del archivo.

  • Directorysrc/content/
    • Directorydocs/ esta colleción usa subdirectorios para organizar por idioma
      • Directoryen/
      • Directoryes/
      • Directoryde/

Para aprovechar al máximo tus colecciones de contenido, crea un archivo src/content/config.ts en tu proyecto (también se admiten las extensiones .js y .mjs). Este es un archivo especial que Astro cargará y utilizará automáticamente para configurar tus colecciones de contenido.

src/content/config.ts
// 1. Importa las utilidades de `astro:content`
import { defineCollection } from 'astro:content';
// 2. Define tu colección(es)
const blogCollection = defineCollection({ /* ... */ });
// 3. Exporta un único objeto `collections` para registrar tu(s) colección(es)
// Esta clave debe coincidir con el nombre de tu directorio de colección en "src/content"
export const collections = {
'blog': blogCollection,
};

Si no extiendes las configuraciones recomendadas de TypeScript strict o strictest de Astro en tu archivo tsconfig.json, es posible que debas actualizar tu tsconfig.json para habilitar strictNullChecks.

tsconfig.json
{
// Nota: No se necesita ningún cambio si usas "astro/tsconfigs/strict" o "astro/tsconfigs/strictest"
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"strictNullChecks": true
}
}

Si usas archivos .js o .mjs en un proyecto de Astro, puedes habilitar IntelliSense y la comprobación de tipos en tu editor habilitando allowJs en tu tsconfig.json:

tsconfig.json
{
// Nota: No se necesita ningún cambio si usas "astro/tsconfigs/strict" o "astro/tsconfigs/strictest"
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"strictNullChecks": true,
"allowJs": true
}
}

Los esquemas garantizan un frontmatter o datos de entrada consistentes dentro de una colección. Un esquema garantiza que estos datos existen en una forma predecible cuando necesitas hacer referencia o consultarlos. Si algún archivo viola su esquema de colección, Astro proporcionará un error útil para informarte.

Los esquemas también potencian las generación automática de tipos de TypeScript para tu contenido en Astro. Cuando defines un esquema para tu colección, Astro generará y aplicará automáticamente una interfaz de TypeScript. El resultado es un soporte completo de TypeScript cuando consultas tu colección, incluyendo el autocompletado de propiedades y la comprobación de tipos.

Para definir tu primera colección, crea un archivo src/content/config.ts si no existe (las extensiones .js y .mjs también son compatibles). Este archivo debe:

  1. Importar las utilidades adecuadas de astro:content.
  2. Definir cada colección que deseas validar. Esto incluye un type (introducido en Astro v2.5.0) que especifica si la colección contiene formatos de autoría de contenido como Markdown (type: 'content') o formatos de datos como JSON o YAML (type: 'data'). También incluye un schema que define la forma de tu frontmatter o datos de entrada.
  3. Exportar un único objeto collections para registrar tus colecciones.
src/content/config.ts
// 1. Importar las utilidades de `astro:content`
import { z, defineCollection } from 'astro:content';
// 2. Definir un `type` y `schema` para cada colección
const blogCollection = defineCollection({
type: 'content', // v2.5.0 y posteriores
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
image: z.string().optional(),
}),
});
// 3. Exportar un único objeto `collections` para registrar tu(s) colección(es)
export const collections = {
'blog': blogCollection,
};

Puedes usar defineCollection() tantas veces como desees para crear múltiples esquemas. Todas las colecciones deben ser exportadas desde dentro del único objeto collections.

src/content/config.ts
const blogCollection = defineCollection({
type: 'content',
schema: z.object({ /* ... */ })
});
const newsletter = defineCollection({
type: 'content',
schema: z.object({ /* ... */ })
});
const authors = defineCollection({
type: 'data',
schema: z.object({ /* ... */ })
});
export const collections = {
'blog': blogCollection,
'newsletter': newsletter,
'authors': authors,
};

A medida que tu proyecto crece, también eres libre de reorganizar tu base de código y mover la lógica fuera del archivo src/content/config.ts. Definir tus esquemas por separado puede ser útil para reutilizar esquemas en múltiples colecciones y compartir esquemas con otras partes de tu proyecto.

src/content/config.ts
// 1. Importar tus utilidades y esquemas
import { defineCollection } from 'astro:content';
import { blogSchema, authorSchema } from '../schemas';
// 2. Definir tus colecciones
const blogCollection = defineCollection({
type: 'content',
schema: blogSchema,
});
const authorCollection = defineCollection({
type: 'data',
schema: authorSchema,
});
// 3. Exportar múltiples colecciones para registrarlas
export const collections = {
'blog': blogCollection,
'authors': authorCollection,
};

Usando esquemas de colección de terceros

Sección titulada Usando esquemas de colección de terceros

Puedes importar esquemas de colección desde cualquier lugar, incluyendo paquetes npm externos. Esto puede ser útil cuando trabajas con temas y bibliotecas que proporcionan sus propios esquemas de colección para que los uses.

src/content/config.ts
import { blogSchema } from 'my-blog-theme';
const blogCollection = defineCollection({ type: 'content', schema: blogSchema });
// Exportar la colección de blog, usando un esquema externo de 'my-blog-theme'
export const collections = {
'blog': blogCollection,
};

Astro usa Zod para potenciar sus esquemas de contenido. Con Zod, Astro puede validar el frontmatter de cada archivo dentro de una colección y proporcionar tipos automáticos de TypeScript cuando consultas el contenido desde dentro de tu proyecto.

Para usar Zod en Astro, importa la utilidad z de "astro:content". Esta es una re-exportación de la biblioteca Zod, y admite todas las funciones de Zod. Consulta el README de Zod para obtener documentación completa sobre cómo funciona Zod y qué funciones están disponibles.

// Ejemplo: Una hoja de trucos de muchos tipos de datos Zod comunes
import { z, defineCollection } from 'astro:content';
defineCollection({
schema: z.object({
isDraft: z.boolean(),
title: z.string(),
sortOrder: z.number(),
image: z.object({
src: z.string(),
alt: z.string(),
}),
author: z.string().default('Anonymous'),
language: z.enum(['en', 'es']),
tags: z.array(z.string()),
// Una propiedad opcional del frontmatter. ¡Muy común!
footnote: z.string().optional(),
// En el frontmatter, las fechas escritas sin comillas se interpretan como objetos Date
publishDate: z.date(),
// También puedes transformar un string de fecha (por ejemplo, "2022-07-08") a un objeto Date
// publishDate: z.string().transform((str) => new Date(str)),
// Avanzado: Valida que el string también sea un correo electrónico
authorContact: z.string().email(),
// Avanzado: Valida que el string también sea una URL
canonicalURL: z.string().url(),
})
})

Definiendo referencias de colección

Sección titulada Definiendo referencias de colección

Las entradas de una colección pueden “referenciar” otras entradas relacionadas.

Con la función reference() de la API de Colecciones, puedes definir una propiedad en un esquema de colección como una entrada de otra colección. Por ejemplo, puedes requerir que cada entrada space-shuttle incluya una propiedad pilot que utilice el propio esquema de la colección pilot para la comprobación de tipos, el autocompletado y la validación.

Un ejemplo común es una publicación de blog que hace referencia a perfiles de autor reutilizables almacenados como JSON, o a URL de publicaciones relacionadas almacenadas en la misma colección:

import { defineCollection, reference, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
// Referencia a un único autor de la colección `authors` por `id`
author: reference('authors'),
// Referencia a un arreglo de publicaciones relacionadas de la colección `blog` por `slug`
relatedPosts: z.array(reference('blog')),
})
});
const authors = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
portfolio: z.string().url(),
})
});
export const collections = { blog, authors };

Este ejemplo de publicación de blog especifica los slug de las publicaciones relacionadas y el id del autor de la publicación:

src/content/blog/welcome.md
---
title: "Bienvenido a mi Blog"
author: ben-holmes # referencia `src/content/authors/ben-holmes.json`
relatedPosts:
- about-me # referencia `src/content/blog/about-me.md`
- my-year-in-review # referencia `src/content/blog/my-year-in-review.md`
---

Cuando usas type: 'content', cada entrada de contenido genera una propiedad slug compatible con URL a partir de su id de archivo. El slug se utiliza para consultar la entrada directamente desde tu colección. También es útil cuando creas nuevas páginas y URL a partir de tu contenido.

Puedes anular el slug generado de una entrada añadiendo tu propia propiedad slug al frontmatter del archivo. Esto es similar a la función “permalink” de otros frameworks web. "slug" es un nombre de propiedad especial y reservado que no está permitido en tu schema de colección personalizado y no aparecerá en la propiedad data de tu entrada.

---
title: Mi Publicación de Blog
slug: my-custom-slug/supports/slashes
---
El contenido de tu publicación de blog aquí.

Astro proporciona dos funciones para consultar una colección y devolver una (o más) entradas de contenido: getCollection() y getEntry().

import { getCollection, getEntry } from 'astro:content';
// Obtén todas las entradas de una colección.
// Requiere el nombre de la colección como argumento.
// Ejemplo: recupera `src/content/blog/**`
const allBlogPosts = await getCollection('blog');
// Obtén una única entrada de colleción.
// Requiere el nombre de la colección y también
// el `slug` de la entrada (colecciones de contenido) o `id` (colecciones de datos)
// Ejemplo: recupera `src/content/authors/grace-hopper.json`
const graceHopperProfile = await getEntry('authors', 'grace-hopper');

Ambas funciones devuelven entradas de contenido tal como se definen en el tipo CollectionEntry.

Accediendo a los datos referenciados

Sección titulada Accediendo a los datos referenciados

Cualquier referencia definida en tu esquema debe consultarse por separado después de consultar por primera vez la entrada de tu colección. Puedes usar la función getEntry() de nuevo, o getEntries(), para recuperar la entrada referenciada del objeto data devuelto.

src/pages/blog/welcome.astro
---
import { getEntry, getEntries } from 'astro:content';
const blogPost = await getEntry('blog', 'welcome');
// Resuelve una referencia singular
const author = await getEntry(blogPost.data.author);
// Resuelve un arreglo de referencias
const relatedPosts = await getEntries(blogPost.data.relatedPosts);
---
<h1>{blogPost.data.title}</h1>
<p>Autor: {author.data.name}</p>
<!-- ... -->
<h2>También te puede gustar:</h2>
{relatedPosts.map(p => (
<a href={p.slug}>{p.data.title}</a>
))}

getCollection() toma un callback opcional “filter” que te permite filtrar tu consulta en función de las propiedades id o data (frontmatter) de una entrada. Para las colecciones de type: 'content', también puedes filtrar en función de slug.

Puedes usar esto para filtrar por cualquier criterio de contenido que desees. Por ejemplo, puedes filtrar por propiedades como draft para evitar que se publiquen publicaciones de blog en borrador en tu blog:

// Ejemplo: Filtra las entradas de contenido con `draft: true`
import { getCollection } from 'astro:content';
const publishedBlogEntries = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});

También puedes crear páginas en borrador que estarán disponibles cuando ejecutes el servidor de desarrollo, pero que no se construirán en producción:

// Ejemplo: Filtrar las entradas de contenido con `draft: true` solo al construir para producción
import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});

Para filtrar argumentos también admite el filtrado por directorios anidados dentro de una colección. Dado que el id incluye la ruta anidada completa, puedes filtrar por el inicio de cada id para devolver solo los elementos de un directorio anidado específico:

// Ejemplo: Filtra las entradas por subdirectorio en la colección
import { getCollection } from 'astro:content';
const englishDocsEntries = await getCollection('docs', ({ id }) => {
return id.startsWith('en/');
});

Usando contenido en plantillas de Astro

Sección titulada Usando contenido en plantillas de Astro

Una vez que hayas consultado tus entradas de colección, puedes acceder a cada entrada directamente dentro de la plantilla de tu componente de Astro. Esto te permite renderizar HTML para cosas como enlaces a tu contenido (usando el slug de contenido) o información sobre tu contenido (usando la propiedad data).

Para obtener información sobre cómo renderizar tu contenido a HTML, consulta Renderizando contenido a HTML a continuación.

src/pages/index.astro
---
import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog');
---
<ul>
{blogEntries.map(blogPostEntry => (
<li>
<a href={`/my-blog-url/${blogPostEntry.slug}`}>{blogPostEntry.data.title}</a>
<time datetime={blogPostEntry.data.publishedDate.toISOString()}>
{blogPostEntry.data.publishedDate.toDateString()}
</time>
</li>
))}
</ul>

Un componente también puede pasar un contenido completo como una prop.

Si haces esto, puedes usar la utilidad CollectionEntry para tipar correctamente las props de tu componente usando TypeScript. Esta utilidad toma un argumento de tipo string que coincide con el nombre del esquema de tu colección y heredará todas las propiedades de ese esquema de colección.

src/components/BlogCard.astro
---
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'blog'>;
}
// `post` coincidirá con el tipo de esquema de tu colección 'blog'
const { post } = Astro.props;
---

Una vez consultado, puedes renderizar las entradas de Markdown y MDX a HTML usando la propiedad de función render() de la entrada. Llamar a esta función te da acceso al contenido y metadatos renderizados, incluyendo tanto un componente <Content /> como una lista de todos los encabezados renderizados.

src/pages/render-example.astro
---
import { getEntry } from 'astro:content';
const entry = await getEntry('blog', 'post-1');
const { Content, headings } = await entry.render();
---
<p>Publicado el: {entry.data.published.toDateString()}</p>
<Content />

Las Colecciones de contenido se almacenan fuera del directorio src/pages/. Esto significa que no se generan rutas para los elementos de tu colección de forma predeterminada. Deberás crear manualmente una nueva ruta dinámica para generar páginas HTML a partir de las entradas de tu colección. Tu ruta dinámica asignará el parámetro de solicitud entrante (por ejemplo, Astro.params.slug en src/pages/blog/[...slug].astro) para recuperar la entrada correcta dentro de una colección.

El método exacto para generar rutas dependerá del modo de salida de tu compilación output: ‘static’ (el valor predeterminado) o ‘server’ (para SSR).

Construyendo para salida estática (predeterminado)

Sección titulada Construyendo para salida estática (predeterminado)

Si estás construyendo un sitio web estático (el comportamiento predeterminado de Astro), debes usar la función getStaticPaths() para crear múltiples páginas a partir de un solo componente src/pages/ durante tu compilación.

Llama getCollection() dentro de getStaticPaths() para consultar tu contenido o tu colección de datos. Luego, crea tus nuevas rutas URL usando la propiedad slug (colecciones de contenido) o la propiedad id(colecciones de datos) para cada entrada de contenido.

src/pages/posts/[...slug].astro
---
import { getCollection } from 'astro:content';
// 1. Genera una nueva ruta para cada entrada de colección
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}
// 2. Para tu plantilla, puedes obtener la entrada directamente de la prop
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<h1>{entry.data.title}</h1>
<Content />

Esto generará una nueva página para cada entrada en la colección blog. Por ejemplo, una entrada en src/content/blog/hello-world.md tendrá un slug de hello-world, y por lo tanto su URL final será /posts/hello-world/.

Construyendo para salida del servidor (SSR)

Sección titulada Construyendo para salida del servidor (SSR)

Si estás construyendo un sitio web dinámico (usando el soporte SSR de Astro), no se espera que generes ninguna ruta de antemano durante la compilación. En su lugar, tu página debe examinar la solicitud (usando Astro.request o Astro.params) para encontrar el slug bajo demanda, y luego recuperarlo usando getEntry().

src/pages/posts/[...slug].astro
---
import { getEntry } from "astro:content";
// 1. Obtén el slug de la solicitud entrante del servidor
const { slug } = Astro.params;
if (slug === undefined) {
throw new Error("Slug es requerido");
}
// 2. Consulta la entrada directamente usando el slug de la solicitud
const entry = await getEntry("blog", slug);
// 3. Redirige si la entrada no existe
if (entry === undefined) {
return Astro.redirect("/404");
}
// 4. (Opcional) Renderiza la entrada a HTML en la plantilla
const { Content } = await entry.render();
---

Migrando desde el Enrutamiento Basado en Archivos

Sección titulada Migrando desde el Enrutamiento Basado en Archivos

Si tienes un proyecto de Astro existente como un blog, que usa archivos Markdown o MDX en subcarpetas dentro de src/pages/, considera migrar los archivos de contenido o datos relacionados a las colecciones de contenido.

Consulta cómo convertir un ejemplo básico de blog de src/pages/posts/ a src/content/posts en nuestro tutorial paso a paso que utiliza la base de código del proyecto terminado del tutorial Crear un Blog.

Habilitando la Generación de Esquemas JSON

Sección titulada Habilitando la Generación de Esquemas JSON

Agregado en: astro@4.13.0

Si estás trabajando con colecciones de tipo data, Astro generará automáticamente archivos de esquema JSON para que tu editor obtenga IntelliSense y comprobación de tipos. Se creará un archivo separado para cada colección de datos en tu proyecto basado en las colecciones definidas en src/content/config.ts utilizando una biblioteca llamada zod-to-json-schema.

Esta carácateristica requiere que establezcas manualmente la ruta del archivo de tu esquema como el valor de $schema en cada archivo de entrada de datos de la colección:

src/content/authors/armand.json
{
"$schema": "../../../.astro/collections/authors.schema.json",
"name": "Armand",
"skills": ["Astro", "Starlight"]
}

Alternativamente, puedes establecer este valor en la configuración de tu editor. Por ejemplo, para establecer este valor en la configuración json.schemas de VSCode, proporciona la ruta de los archivos a coincidir y la ubicación de tu esquema JSON:

{
"json.schemas": [
{
"fileMatch": [
"/src/content/authors/**"
],
"url": "./.astro/collections/authors.schema.json"
}
]
}

Habilitando el Caché de Construcción

Sección titulada Habilitando el Caché de Construcción

Agregado en: astro@3.5.0 Experimental

Si estás trabajando con colecciones grandes, es posible que desees habilitar las compilaciones en caché con la bandera experimental.contentCollectionCache. Esta característica experimental optimiza el proceso de compilación de Astro, permitiendo que las colecciones no modificadas se almacenen y reutilicen entre compilaciones.

En muchos casos, esto puede conducir a mejoras significativas en el rendimiento de la compilación.

Mientras esta característica se estabiliza, es posible que te encuentres con problemas con la caché almacenada. Siempre puedes restablecer tu caché de compilación ejecutando el siguiente comando:

npm run astro build -- --force

Modificando el Frontmatter con Remark

Sección titulada Modificando el Frontmatter con Remark

Astro admite los plugins remark o rehype que modifican tu frontmatter directamente. Puedes acceder a este frontmatter modificado dentro de una entrada de contenido usando la propiedad remarkPluginFrontmatter devuelta de render():

---
import { getEntry } from 'astro:content';
const blogPost = await getEntry('blog', 'post-1');
const { remarkPluginFrontmatter } = await blogPost.render();
---
<p>{blogPost.data.title}{remarkPluginFrontmatter.readingTime}</p>
Receta relacionada: Agregar tiempo de lectura

Los pipelines de remark y rehype solo se ejecutan cuando se renderiza tu contenido, lo que explica por qué remarkPluginFrontmatter solo está disponible después de llamar a render() en tu entrada de contenido. En contraste, getCollection() y getEntry() no pueden devolver estos valores directamente porque no renderizan tu contenido.

Trabajando con fechas en el frontmatter

Sección titulada Trabajando con fechas en el frontmatter

Muchos formatos de fecha son admitidos en las colecciones de contenido, pero el esquema de tu colección debe coincidir con el formato utilizado en el frontmatter YAML de tu Markdown o MDX.

YAML usa el estándar ISO-8601 para expresar fechas. Utiliza el formato yyyy-mm-dd (por ejemplo, 2021-07-28) junto con un tipo de esquema de z.date():

src/pages/posts/example-post.md
---
title: Mi Publicación de Blog
pubDate: 2021-07-08
---

El formato de fecha se especificará en UTC si no se proporciona una zona horaria. Si necesitas especificar una zona horaria, puedes usar el formato ISO 8601.

src/pages/posts/example-post.md
---
title: Mi Publicación de Blog
pubDate: 2021-07-08T12:00:00-04:00
---

Para renderizar solo YYYY-MM-DD de la marca de tiempo UTC completa, utiliza el método slice de JavaScript para eliminar la marca de tiempo:

src/layouts/ExampleLayout.astro
---
const { frontmatter } = Astro.props;
---
<h1>{frontmatter.title}</h1>
<p>{frontmatter.pubDate.toISOString().slice(0,10)}</p>

Para ver un ejemplo usando toLocaleDateString para formatear el día, mes y año, consulta el componente <FormattedDate /> en la plantilla oficial del blog de Astro.

Contribuir

¿Qué tienes en mente?

Comunidad
京ICP备15031610号-99