logo cosasdedevs
Parte 1: Cargar posts en la página de inicio y detalle del post con Svelte 5 y Sveltekit

Parte 1: Cargar posts en la página de inicio y detalle del post con Svelte 5 y Sveltekit



My Profile
Ene 07, 2025

En esta sección del tutorial, vamos a integrar la funcionalidad para cargar dinámicamente los posts desde una base de datos en la página de inicio de nuestro blog. 

Además, crearemos una página de detalle para mostrar información específica de cada post cuando los usuarios hagan clic en ellos. Esto nos permitirá entender cómo manejar rutas dinámicas y obtener datos específicos utilizando las herramientas nativas de SvelteKit.

Antes de empezar 🛑 y como siempre, si te has perdido alguno de los tutoriales de esta serie, aquí te dejo todos los tutoriales escritos hasta ahora:

Añadir nuevos estilos y assets

El primer paso a realizar es añadir nuevos estilos al archivo, app.css donde almacenamos los estilos generales que pueden ser utilizados en toda la aplicación. Para ello, abrimos el archivo src\app.css y añadimos las siguientes líneas:

a {
    color: #347cbb;
}


a:hover {
    color: #FFAA00;
}


.main-container {
    max-width: 800px;
    margin: 0 auto;
    text-align: center;
}
    
.post {
    padding: 0 1rem;
}


.image-post img {
    max-width: 100%;
    object-fit: cover;
}


.content-post {
    margin: 1rem 0;
    max-width: 750px;
    text-align: justify;
    padding: 0 1rem;
}


.content-post p {
    margin-bottom: 1rem;
}


.separator {
    border-bottom: 1px solid #9d9d9d;
    margin: 1rem 0;
}

Básicamente, hemos definido unos colores para los links, un contenedor principal para todas nuestras páginas, estilos para los artículos de nuestro blog y un separador que añadirá una línea gris horizontal donde la necesitemos.

También vamos a añadir una nueva imagen dentro de src\lib\assets que utilizaremos como imagen por defecto cuando uno de nuestros usuarios no tenga un avatar.

https://github.com/albertorc87/blog-svelte-5/blob/tutorial-4-pagina-principal-lista-posts/src/lib/assets/default-avatar.jpg

Añadir campo para almacenar el avatar en la tabla usuarios

En el paso anterior se me olvidó añadir el campo donde almacenar el avatar del usuario, así que tendremos que editar el esquema de la base de datos. Abrimos el archivo src\lib\server\db\schema.ts y añadimos el campo avatar que será de tipo varchar y con una longitud máxima de 255 caracteres. Al igual que para los posts, en él guardaremos la ruta donde está almacenado.

export const usersTable = pgTable("users", {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    username: varchar({ length: 255 }).notNull(),
    password: varchar({ length: 255 }).notNull(),
    email: varchar({ length: 255 }).notNull().unique(),
    createdAt: timestamp("created_at", {
        withTimezone: true,
        mode: "date"
    }).defaultNow().notNull(),
    isActive: boolean("is_active").default(true).notNull(),
    isAdmin: boolean("is_admin").default(false).notNull(),
    avatar: varchar('avatar', { length: 255 }),
});

Para añadir el campo en la tabla, debemos volver a lanzar el siguiente comando que subirá el cambio realizado en el esquema:

npx drizzle-kit push

Cargar artículos en la página principal

La idea es que, al acceder a la página principal, se carguen automáticamente los 5 últimos posts publicados que no estén en borradores. 

Implementaremos una funcionalidad de scroll infinito para que, al llegar al final de la página, se carguen los siguientes 5 posts, continuando así hasta que no haya más artículos disponibles. 

Cada post mostrará su imagen destacada, título y el primer párrafo, junto con botones para compartir en Twitter y Facebook. Además, incluiremos el avatar del autor, su nombre de usuario y la fecha de publicación de cada post.

Modificar el archivo layout.ts

Vamos a editar el archivo src/routes/+layout.svelte, que, como recordarás, es donde definimos todos los elementos comunes de nuestra web. 

En este archivo, envolveremos el contenido de los componentes hijos (children) dentro de una etiqueta <main> con la clase main-container

Esta clase, que creamos anteriormente, asegura que todos los componentes hijos de nuestra aplicación compartan el mismo tamaño y estilo de contenedor.

<main class="main-container">
{@render children()}
</main>

Funciones para trabajar con nuestros artículos

Para mantener nuestro proyecto organizado y facilitar la reutilización de funciones, vamos a crear un archivo llamado src/lib/server/db/posts.ts, donde almacenaremos todas las funciones relacionadas con los posts y la base de datos.

Antes de continuar, necesitamos renombrar el archivo src/lib/server/db/index.ts a src/lib/server/db/db.ts. Aunque inicialmente lo copié así de la documentación, considero que nombrarlo db es más intuitivo y refleja mejor su propósito.

Una vez creado el archivo src/lib/server/db/posts.ts añadiremos el siguiente contenido:

import { eq, and, type InferSelectModel, desc } from 'drizzle-orm';
import { postsTable, usersTable } from './schema';
import { db } from './db';


export type PostWithUser = Pick<InferSelectModel<typeof postsTable>,
    'title' | 'content' | 'createdAt' | 'imageHeader' | 'slugUrl'
> & {
    username: string;
    avatar: string | null;
};


export async function getPostBySlugUrl(slugUrl: string): Promise<PostWithUser | undefined> {
    const result = await db
        .select({
            title: postsTable.title,
            content: postsTable.content,
            createdAt: postsTable.createdAt,
            imageHeader: postsTable.imageHeader,
            slugUrl: postsTable.slugUrl,
            username: usersTable.username,
            avatar: usersTable.avatar,
        })
        .from(postsTable)
        .innerJoin(usersTable, eq(postsTable.userId, usersTable.id))
        .where(and(eq(postsTable.isDraft, false), eq(postsTable.slugUrl, slugUrl)));


    return result[0];
}


export async function getPosts(limit: number = 5, offset: number = 0): Promise<PostWithUser[]> {
    return await db.select({
        title: postsTable.title,
        content: postsTable.content,
        createdAt: postsTable.createdAt,
        imageHeader: postsTable.imageHeader,
        slugUrl: postsTable.slugUrl,
        username: usersTable.username,
        avatar: usersTable.avatar,
    }).from(postsTable)
        .innerJoin(usersTable, eq(postsTable.userId, usersTable.id))
        .where(eq(postsTable.isDraft, false)).orderBy(desc(postsTable.createdAt)).limit(limit).offset(offset);
}

Con este código nos encargamos de recuperar los posts de la base de datos según nuestras necesidades específicas.

  1. Importaciones:
    • Importamos funciones clave de Drizzle ORM como operadores y tipos necesarios para construir nuestras consultas.
    • Cargamos los esquemas de las tablas usersTable y postsTable, que representan la estructura de las tablas en nuestra base de datos.
    • Importamos la conexión a la base de datos desde el archivo db.
  2. Definición del tipo PostWithUser:
    • Creamos un tipo que combina campos específicos de postsTable (title, content, createdAt, imageHeader, slugUrl) con los campos username y avatar provenientes de usersTable. Este tipo nos permite estructurar y manejar datos que combinan información de ambas tablas.
  3. Función getPostBySlugUrl:
    • Recupera un único post identificándolo por su slugUrl.
    • Comprueba que el post no esté en borradores (isDraft = false).
    • Selecciona y devuelve los campos necesarios tanto del post como del usuario relacionado.
  4. Función getPosts:
    • Recupera una lista de posts paginados con soporte para límites (limit) y desplazamiento (offset), útil para implementar funcionalidades como scroll infinito.
    • Asegura que los posts no sean borradores y los ordena por fecha de creación en orden descendente.
    • Devuelve los campos necesarios de cada post junto con los datos del usuario que lo publicó.

Utilidades para el contenido HTML

Para mostrar únicamente el primer párrafo de cada post, añadiremos una función dedicada a esta tarea. Para organizar mejor el proyecto, crearemos una carpeta llamada utils dentro de src\lib. En esta carpeta, generaremos un archivo llamado html.ts, donde almacenaremos esta función y, en el futuro, otras funciones relacionadas con la manipulación de HTML. El contenido inicial del archivo será el siguiente:

export function getFirstParagraph(htmlContent: string): string {
    const match = htmlContent.match(/<p>.*?<\/p>/s);
    return match ? match[0] : '';
}

Recuperar los últimos 5 posts para la página principal

En un proyecto con Svelte 5 y SvelteKit, para realizar acciones del lado del servidor, como recuperar los posts, utilizamos un tipo especial de archivos llamados +page.server.ts. Estos archivos se ubican en la ruta correspondiente a la página donde necesitamos realizar dichas acciones y permiten generar información que luego podemos acceder desde los archivos +page.svelte.

Dado que ahora necesitamos esta funcionalidad para la página principal, crearemos el archivo en la raíz de la carpeta routes, con la siguiente ruta: src\routes\+page.server.ts. El contenido inicial de este archivo será:

import type { PageServerLoad } from './$types';
import { getPosts } from '$lib/server/db/posts';
import { getFirstParagraph } from '$lib/utils/html';


export const load: PageServerLoad = async () => {


    const posts = await getPosts();


    return {
        posts: posts.map((post) => {
            post.content = getFirstParagraph(post.content);
            return post;
        })
    }

};

Este código define la función load, que se ejecuta en el servidor para cargar datos y enviarlos a la página +page.svelte. En ella usamos la función creamos anteriormente para recuperar los últimos posts y después utilizamos la función map de JavaScript para sustituir el contenido de post.content por el primer párrafo.

Crear un componente para los artículos de la página principal

Dado que los artículos de la página principal y los que se mostrarán en detalle tendrán formatos diferentes, vamos a separarlos en dos componentes independientes. Sin embargo, ambos compartirán una parte en común: el pie del artículo. Para mantener el código organizado y reutilizable, crearemos un componente específico para esta sección compartida.

Ya que en el pie de página vamos a mostrar dos iconos para el botón de compartir por Twitter y por Facebook, vamos a instalar la librería Iconify que nos permite acceder a miles de iconos. Para instalar la versión para Svelte, lanzaremos el siguiente comando desde la terminal:

npm install --save-dev @iconify/svelte

Ahora vamos a crear el componente para el pie de los artículos. Para ello, añadiremos una nueva carpeta llamada Article dentro de src\lib\components. Dentro de esta carpeta, crearemos un archivo llamado ArticleFooter.svelte, que contendrá la lógica necesaria para ser utilizado posteriormente en ambos componentes de artículos. El contenido inicial del archivo será el siguiente:

<script lang="ts">
    import { page } from "$app/stores";
    import Icon from "@iconify/svelte";
    import defaultAvatar from '$assets/default-avatar.jpg';


    interface ArticleFooterProps {
        slug: string;
        username: string;
        createdAt: Date;
        avatar: string | null;
    }
    let { slug, username, createdAt, avatar }: ArticleFooterProps = $props();


    const postUrl = encodeURIComponent(`${$page.url.origin}/blog/${slug}`)


    const urlFacebook = `https://www.facebook.com/sharer/sharer.php?u=${postUrl}`;
    const urlTwitter = `https://twitter.com/intent/tweet?text=${postUrl}`;
</script>


<div class="separator"></div>
<div class="footer-post">
    <div class="footer-post-content-user">
        <div><img src={avatar || defaultAvatar} alt="Avatar" /></div>
        <div class="footer-post-content-user-info">
            <div><strong>{username}</strong></div>
            <div>
                {createdAt.toLocaleDateString('en-US', {
                    day: '2-digit',
                    month: 'short',
                    year: 'numeric'
                })}
            </div>
        </div>
    </div>
    <div class="footer-post-content-social">
        <a href={urlTwitter} target="_blank" rel="noopener noreferrer">
            <Icon icon="entypo-social:twitter-with-circle" width={'35'} />
        </a>
        <a href={urlFacebook} target="_blank" rel="noopener noreferrer">
            <Icon icon="entypo-social:facebook-with-circle" width={'35'} />
        </a>
    </div>
</div>


<style>
    
    .footer-post {
        width: 100%;
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 2.5rem;
    }


    .footer-post img {
        width: 35px;
        height: 35px;
        cursor: pointer;
        border-radius: 50%;
        object-fit: cover;
    }


    .footer-post-content-user {
        display: flex;
        gap: 0.7rem;
        align-items: center;
    }


    .footer-post-content-user-info {
        display: flex;
        text-align: left;
        flex-direction: column;
        font-size: 0.7rem;
    }
</style>

El código realiza las siguientes acciones:

  1. Importaciones necesarias:
    • page de SvelteKit: Lo usamos para obtener información sobre la URL actual de la página y construir la ruta completa del post.
    • Icon de la librería Iconify: Permite añadir iconos fácilmente.
    • defaultAvatar: Una imagen predeterminada que se usará si el usuario no tiene un avatar asignado.
  2. Interfaz de validación:
    Creamos la interfaz ArticleFooterProps para asegurarnos de que las propiedades del componente (slug, username, createdAt y avatar) cumplen con los tipos esperados. Esto ayuda a evitar errores y facilita la lectura del código. Vas a ver que estas interfaces las utilizaremos a menudo en este tutorial.
  3. Obtención de las propiedades:
    Recuperamos las propiedades del componente usando $props() y las desestructuramos para trabajar más cómodamente con ellas.
  4. Construcción de las URLs de redes sociales:
    • Creamos postUrl, una versión codificada de la URL del post para asegurar que sea válida al compartirla.
    • A partir de postUrl, construimos las URLs específicas para compartir en Facebook y Twitter (urlFacebook y urlTwitter).
  5. Construcción del HTML:
    • El pie del artículo incluye dos secciones principales:
      • Información del autor: Se muestra el avatar del usuario (o la imagen predeterminada) y el nombre del autor. Además, formateamos la fecha (createdAt) en un formato legible utilizando toLocaleDateString con opciones específicas.
      • Botones para compartir: Creamos enlaces que abren las redes sociales en una nueva pestaña, con iconos de Facebook y Twitter personalizados desde Iconify.
  6. Uso de Iconify:
    Los iconos se seleccionan mediante el atributo icon, especificando el nombre desde la librería Iconify. Puedes consultar su web para explorar y seleccionar otros iconos.
  7. Estilos del componente:
    • Diseñamos el pie del artículo para que sea visualmente atractivo, con un diseño flexible usando flexbox.
    • Ajustamos el tamaño del avatar, lo redondeamos, y aseguramos que se recorte correctamente con object-fit: cover.
    • La información del usuario y los botones de redes sociales están distribuidos de forma clara y con espaciado adecuado.

El siguiente paso es crear el archivo ArticleMain.svelte. Este lo crearemos también dentro de src\lib\components\Article y tendrá el siguiente contenido:

<script lang="ts">
    import { ArticleFooter } from '$components';
    import type { PostWithUser } from '$lib/server/db/posts';


    let { post }: { post: PostWithUser } = $props();
</script>


<article class="post">
    <div class="image-post">
        <a href={`/blog/${post.slugUrl}`}>
            <img src={post.imageHeader} alt="" />
        </a>
    </div>
    <div class="title-post">
        <a href={`/blog/${post.slugUrl}`}>
            <h2>{post.title}</h2>
        </a>
    </div>
    <div class="content-post">
        {@html post.content}
    </div>
    <ArticleFooter
        slug={post.slugUrl}
        username={post.username}
        createdAt={post.createdAt}
        avatar={post.avatar}
    />
</article>


<style>
    .title-post h2 {
        margin-top: 1rem;
        font-size: 2.5rem;
    }
</style>

En este código, creamos un componente para mostrar un artículo del blog, incorporando su imagen, título, contenido y un pie de artículo personalizado. A continuación, te explico cada parte del código:

  1. Importaciones esenciales:
    • Importamos el componente ArticleFooter, que ya definimos previamente, para mostrar la información del autor y las opciones de compartir el post.
    • Importamos el tipo PostWithUser desde la carpeta src\lib\server\db\posts.ts. Esto nos permite usarlo como tipo de las propiedades que recibe el componente, asegurando que los datos sean consistentes y estén bien definidos.
  2. Propiedades del componente:
    Utilizamos $props() para recibir la información del artículo a través de la variable post, que está tipada como PostWithUser. Esto ayuda a que el código sea más robusto y comprensible.
  3. Construcción del HTML:
    • Imagen del artículo: Mostramos la imagen de encabezado del post (post.imageHeader) como un enlace hacia la página del artículo.
    • Título del artículo: Incluimos un encabezado <h2> con el título del post (post.title), también enlazado a su URL.
    • Contenido del artículo: Usamos la directiva {@html} para renderizar el contenido HTML del post, lo que permite mostrar el primer párrafo directamente desde los datos del artículo.
    • Pie del artículo: Integramos el componente ArticleFooter, pasando las propiedades necesarias (slug, username, createdAt y avatar) para completar su funcionalidad.
  4. Estilos personalizados:
    • Agregamos un estilo específico para el título del artículo, ajustando el margen superior y el tamaño de fuente para destacar visualmente el encabezado.

En este paso, al igual que en el tutorial anterior, centralizamos las exportaciones de los componentes en el archivo src/lib/components/index.ts.

En el archivo index.ts, añadimos las siguientes líneas:

export {default as Header} from '$components/layout/Header.svelte';
export {default as ArticleMain} from '$components/Article/ArticleMain.svelte';
export {default as ArticleFooter} from '$components/Article/ArticleFooter.svelte';

Puedes continuar la siguiente parte de este tutorial pulsando aquí:

https://cosasdedevs.com/posts/parte-2-cargar-posts-pagina-inicio-detalle-post-svelte-5-sveltekit/

Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.

Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.

Nos leemos 👋.

221 vistas

🐍 Sígueme en Twitter

Si te gusta el contenido que subo y no quieres perderte nada, sígueme en Twitter y te avisaré cada vez que cree contenido nuevo 💪
Luego ¡Te sigo!

Nos tomamos en serio tu privacidad

Utilizamos cookies propias y de terceros para recopilar y analizar datos sobre la interacción de los usuarios con cosasdedevs.com. Ver política de cookies.