DEV Community

Cover image for AstroJS 5.1: Integra contenido de Dev.to de manera sencilla
Juan Martin Ruiz
Juan Martin Ruiz

Posted on • Edited on

1

AstroJS 5.1: Integra contenido de Dev.to de manera sencilla

Esta bueno tener un blog personal, pero hay momentos en que necesitamos hacer una publicación en una red social por diferentes razones. Si no desea duplicar las publicaciones de su blog con una publicación de una red social, podemos traer el contenido de la publicación y usar nuestro sitio web como una fuente principal de contenido, donde incluso podemos unir las diferentes publicaciones en una sola colección de publicaciones para que podamos aplicar filtros más adelante. Además, si no queremos ocupar el proyecto con muchas imágenes en el sistema de archivos local o tenemos que pagar el alojamiento de imágenes, o simplemente no queremos escribir en texto "simple" con Markdown. O, simplemente, en el caso que tenga su homepage desarrollada con Astro y en la seccion de Blog solo quiere traer los post de Hashnode, Wordpress, Medium, etc.

En este caso, lo haremos con Devto.

Tabla de contenido

Requisitos previos

  • Conocimiento fundamental de Typescript y React
  • node.js ≥ 16.12.0
  • Un editor de código VScode (opcional)
  • Git (opcional)

1. ¿Por qué Astro?

Astro es el marco web para construir sitios web orientados al contenido, como blogs, marketing y comercio electrónico. Astro es conocido por ser pionero en una nueva arquitectura frontend llamada "Islas" para reducir la sobrecarga de JavaScript y la complejidad en comparación con otros marcos. Está optimizado por el rendimiento gracias a su estrategia de "cero JavaScript por defecto", solo se cargan los JS necesarios en las partes interactivas, lo que resulta en tiempos de carga rápidas y una excelente optimización de SEO. Es flexible con los marcos/bibliotecas de la interfaz de usuario; Es agnóstico, lo que significa que puede integrar componentes de React, Vue, Svelte, combinando lo mejor de cada uno. Y, por último, su facilidad de uso; Su sintaxis basada en HTML y su sistema de archivos simple hacen que la curva de aprendizaje sea muy suave, incluso si proviene de un fondo más tradicional.

2. Empezar un nuevo proyecto con el template blog

Esto es en caso de que no tenga un sitio creado, si lo hace, la implementación no variará mucho con un proyecto realizado con Astro.

Vamos a crear un blog con la plantilla ofrecida por la comunidad oficial.

npm create astro@latest -- --template blog

Image description

2.1 Levantamos la app

cd [nombre_proyecto] && npm run dev

Image description

index de la pagina

A partir de Astro 5, se ha introducido la Content Layer API, una herramienta que permite cargar datos desde cualquier fuente durante la construcción de tu sitio y acceder a ellos mediante una API sencilla y con tipado seguro.

Esta API ofrece flexibilidad para manejar contenido de diversas fuentes, como archivos locales en Markdown, APIs remotas o sistemas de gestión de contenido (CMS). Al definir "colecciones" de contenido con esquemas específicos, puedes estructurar y validar tus datos de manera eficiente. Además, la Content Layer API mejora el rendimiento en sitios con gran cantidad de contenido, acelerando los tiempos de construcción y reduciendo el uso de memoria.

https://astro.build/blog/astro-5/

3. Content Layer API de Astro para integrar publicaciones de dev.to en tu sitio

Image description

Puedes utilizar la Content Layer API de Astro para integrar publicaciones de dev.to en tu sitio. Aunque no existe un cargador (loader) específico para dev.to, puedes crear uno personalizado que consuma su API y almacene las publicaciones en una colección de contenido en Astro.

Para lograr esto, sigue estos pasos:

3.1. Configura el acceso a la API de dev.to

Image description

Image description

Image description

Crear el archivo .env en el root del proyecto
.env

DEV_TO_API_URL=https://dev.to/api/
DEV_API_KEY=tu_clave_de_api
Enter fullscreen mode Exit fullscreen mode

3.2. Define una colección en Astro

En src/content.config.ts, define una colección para las publicaciones de dev.to utilizando la Content Layer API:

Al crear el proyecto con el template de Astro, nos genera automaticamente la coleccion para el Blog

src\content.config.ts

import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
    // Load Markdown and MDX files in the `src/content/blog/` directory.
    loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
    // Type-check frontmatter using a schema
    schema: z.object({
        title: z.string(),
        description: z.string(),
        // Transform string to Date object
        pubDate: z.coerce.date(),
        updatedDate: z.coerce.date().optional(),
        heroImage: z.string().optional(),
    }),
});

export const collections = { blog };

Enter fullscreen mode Exit fullscreen mode

Ahora creamos la coleccion para lor articlos de Dev.to

const devTo = defineCollection({
    loader: async () => {
        const headers = new Headers({
            "api-key": DEV_API_KEY,
        });
        const posts = await fetch(`${DEV_TO_API_URL}articles/me/published`,   {
            headers: headers
        }).then(res => res.json());

        return posts.map((post: any) => ({
        id: post.slug,
            title: post.title,
            description: post.description,
            pubDate: new Date(post.published_at),
            updatedDate: post.edited_at ? new Date(post.edited_at) : null,
            heroImage: post.cover_image || post.social_image,
        url: post.url,
        }));
    },
    schema: z.object({
        title: z.string(),
        description: z.string(),
        pubDate: z.coerce.date(),
        updatedDate: z.coerce.date().optional(),
        heroImage: z.string().nullable(),
    url: z.string(),
    }),
  });

export const collections = { blog, devTo };
Enter fullscreen mode Exit fullscreen mode

Asi queda el codigo completo de
src\content.config.ts

import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
const { DEV_API_KEY, DEV_TO_API_URL } = import.meta.env;

const blog = defineCollection({
    // Load Markdown and MDX files in the `src/content/blog/` directory.
    loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
    // Type-check frontmatter using a schema
    schema: z.object({
        title: z.string(),
        description: z.string(),
        // Transform string to Date object
        pubDate: z.coerce.date(),
        updatedDate: z.coerce.date().optional(),
        heroImage: z.string().optional(),
    }),
});

const devTo = defineCollection({
    loader: async () => {
        const headers = new Headers({
            "api-key": DEV_API_KEY,
        });
        const posts = await fetch(`${DEV_TO_API_URL}articles/me/published`, {
            headers: headers
        }).then(res => res.json());

        return posts.map((post: any) => ({
            id: post.slug,
            title: post.title,
            description: post.description,
            pubDate: new Date(post.published_at),
            updatedDate: post.edited_at ? new Date(post.edited_at) : null,
            heroImage: post.cover_image || post.social_image,
            url: post.url,
            body_markdown: post.content,
        }));
    },
    schema: z.object({
        title: z.string(),
        description: z.string(),
        pubDate: z.coerce.date(),
        updatedDate: z.coerce.date().optional(),
        heroImage: z.string().nullable(),
        url: z.string(),
        body_markdown: z.string().optional(),
    }),
  });

export const collections = { blog, devTo };
Enter fullscreen mode Exit fullscreen mode

Fíjese el detalle en la definición de los campos en schema, los campos tienen que coincidir con la collection de blog del template de Astro y luego agregar los que son particulares de la collection de los posts de Dev.to. Tienen que tener el mismo nombre el tipo de dato, es para que podamos "fusionar" en la sección de Blog los posts markdown del template de Astro con los de Dev.to.

4. Utiliza las publicaciones en tus páginas:

Ahora puedes acceder a las publicaciones de dev.to en tus componentes o páginas de Astro utilizando getCollection:

Originalmente:

src\pages\blog\index.astro

---
import BaseHead from '../../components/BaseHead.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
import { getCollection } from 'astro:content';
import FormattedDate from '../../components/FormattedDate.astro';

const posts = (await getCollection('blog')).sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<!-- Iteracion los posts -->
<main>
            <section>
                <ul>
                    {
                        posts.map((post) => (
                            <li>
                                <a href={`/blog/${post.id}/`}>
                                    <img width={720} height={360} src={post.data.heroImage} alt="" />
                                    <h4 class="title">{post.data.title}</h4>
                                    <p class="date">
                                        <FormattedDate date={post.data.pubDate} />
                                    </p>
                                </a>
                            </li>
                        ))
                    }
                </ul>
            </section>
        </main>


Enter fullscreen mode Exit fullscreen mode

index de la pagina

Combinamos las colecciones blog de astro y blog de dev.to y volver a ordenar las publicaciones para garantizar que el orden cronológico sea correcto.

src\pages\blog\index.astro

---
import BaseHead from '../../components/BaseHead.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
import { getCollection } from 'astro:content';
import FormattedDate from '../../components/FormattedDate.astro';

const astroPosts = (await getCollection('blog')).sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);

const devtoPosts = (await getCollection("devTo")).sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);

const posts = [...astroPosts, ...devtoPosts]
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

---


Enter fullscreen mode Exit fullscreen mode

Ahora la iteracion de los posts lo haremos con un condicional inline que en caso que sea devto rediriga a la url del articulo del sitio de https://dev.to/{username}/{slug-article}

                    {
                        posts.map((post) => (
                            <li>
                                <a href={post.collection === "devTo" ? post.data.url : `/blog/${post.id}/`}>
                                    <img width={720} height={360} src={post.data.heroImage} alt="" />
                                    <h4 class="title">{post.data.title}</h4>
                                    <p class="date">
                                        <FormattedDate date={post.data.pubDate} />
                                    </p>
                                </a>
                            </li>
                        ))
                    }
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

5. Use post de Devto en la página de Blog Slug (opcional)

En caso de que desee incluir el contenido de la publicación media dentro de Astro, debemos modificar src/content.config.ts, src/pages/blog/index.astro y src/paginas/blog/[... slug].astro


// src\content.config.ts

const devTo = defineCollection({
    loader: async () => {
        const headers = new Headers({
            "api-key": DEV_API_KEY,
        });
        const posts = await fetch(`${DEV_TO_API_URL}articles/me/published`, {
            headers: headers
        }).then(res => res.json());

        return posts.map((post: any) => ({
            id: post.slug,
            title: post.title,
            description: post.description,
            pubDate: new Date(post.published_at),
            updatedDate: post.edited_at,
            heroImage: post.cover_image || post.social_image,
            url: post.url,
            content: post.body_markdown,
        }));
    },
    schema: z.object({
        title: z.string(),
        description: z.string(),
        pubDate: z.coerce.date(),
        updatedDate: z.coerce.date().optional(),
        heroImage: z.string().optional(),
        url: z.string(),
        content: z.string(),
    }),
  });

Enter fullscreen mode Exit fullscreen mode

5.1 Mostrar contenido del Post de Devto dentro de Astro

Necesitamos modificar el src\pages\blog\[…slug].astro para mostrar la publicación en particular.

Dado que nuestro sitio está en modo estático de forma predeterminada, utilizaremos getStaticPaths() para generar las rutas estáticamente en el momento de la compilación para mostrar la publicación en particular.

// src\pages\blog\[…slug].astro

export async function getStaticPaths() {
    const blogPosts = await getCollection('blog');
    const mediumPosts = await getCollection('devTo') || [];

    const posts = [...blogPosts, ...mediumPosts]; 

    return posts.map((post) => ({
        params: { slug: post.id },
        props: post,
    }));
}
Enter fullscreen mode Exit fullscreen mode

Image description

Antes de usar los componentes propios de Astro para mostrar el post de Devto instalamos la libreria marked que nos va servir para parsear con contenido en formato markdown a HTML

# https://www.npmjs.com/package/marked
npm i marked
Enter fullscreen mode Exit fullscreen mode

Astro viene con unas funciones y componentes que sirven para renderizar nuestro contenido markdown tanto locales como de una API externa.

Utilizamos la función Render() que viene con Astro para compilar las publicaciones de blog y <Content /> es como un componente de renderizado en sí, no habría necesidad de crear un componente especializado para representar todo el contenido de Markdown pero solo nos sirve para los markdown locales. En el caso de representar el contenido de publicaciones de Devto, utilizaremos el componente Fragment. <Fragment /> es un componente Astro incorporado que le permite evitar un elemento de envoltura innecesaria. Esto puede ser especialmente útil al obtener HTML de un CMS (por ejemplo, hashnode o WordPress). Como el contenido de Devto es de tipo markdown primero lo parseamos a HTML con la libreria marked

---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
import { render } from 'astro:content';
import { marked } from 'marked';

export async function getStaticPaths() {
    const blogPosts = await getCollection('blog');
    const mediumPosts = await getCollection('devTo') || [];
    const posts = [...blogPosts, ...mediumPosts]; 
    return posts.map((post) => ({
        params: { slug: post.id },
        props: post,
    }));
}

type Props = CollectionEntry<'blog'> | CollectionEntry<'devTo'>;

const post = Astro.props;

const isDevto = post.collection === "devTo";

const htmlContent = marked.parse(isDevto ? post.data.content : '');

const { Content } = await render(post);
---

<BlogPost {...post.data}>
    <!-- renders local Markdown content -->
    <Content />
    <!-- renders the Devto post content directly as HTML -->
    <Fragment set:html={htmlContent } />
    <!-- renders a link to the Devto post at the end of the content -->
    <a 
        href={isDevto ? post.data.url: ''} 
        target="_blank"
    >
        {isDevto && post.data.url}
    </a>
</BlogPost>
Enter fullscreen mode Exit fullscreen mode

Ejecutamos el proyecto

npm run dev
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Image description

6. Nota

Tener en cuenta que si crea un nuevo post desde Dev.to tiene que volver a compilar el proyecto con npm run build desde la plataforma deployada (Netlify, Vercel, Render, Cloudflare) ya que este proyecto web es totalmente estático, por defecto en Astro 5 es estatico (hibrido y estático). En caso que quiera que el sitio sea dinámico NO tiene que hacer el fetch() desde el archivo de content.config.ts, las partes que quiera que sean dinámicas tiene que usar prerender = false e instalar un adaptador para node o de la plataforma que precise. Pero la naturaleza estática de Content Layer es intencional y es parte de lo que hace que sea tan eficiente en términos de rendimiento, pero también es la razón por la que no es adecuado para datos que necesitan actualizarse frecuentemente o en tiempo real.
Otras alternivas es aplicar ISR (Incremental Static Regeneration), este artículo explica muy bien como hacerlo. Otra opción es automatizar el deploy del proyecto con GitHub Actions. Y por último, para no sacrificar el beneficio de un sitio estático, se puede aplicar Deploy Hooks en cualquier plataforma de Hosting.

Repositorio: https://github.com/jmr85/astro-devto

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (1)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay