Blogs
1. Estructura de Archivos del Blog
El desarrollo del blog está organizado en diferentes componentes y estilos reutilizables. La estructura de archivos debería ser algo como esto:
la carpeta noticias cambiarla por blogs si se quieren blogs en vez de noticias
/components/blog/ - HeroBlogs.vue - LastBlogs.vue - NewsStand.vue - OutstandingBlogs.vue
/components/blog/scss/ - mixinCards.scss
/pages/noticias/ - index.vue - [slug].vue2. Creación de Componentes de Blog
2.1 Componente: HeroBlogs.vue
Este componente se utiliza para mostrar una imagen de héroe con el título principal del blog. Incluye el uso de slots para personalizar el fondo y el contenido.
<script setup lang="ts">const TheHero = defineAsyncComponent( () => import("@/components/common/TheHero.vue"));</script>
<template> <TheHero> <template #background> <NuxtImg :src="$t('news.header_image')" :alt="$t('news.header_image_alt')" :title="$t('news.header_image_alt')" class="hero-blog__image" /> </template> <template #content> <h1 class="hero-blog__title">{{ $t("news.header_title") }}</h1> <h2 class="hero-blog__subtitle">{{ $t("news.header_subtitle") }}</h2> </template> </TheHero></template>
<style scoped lang="scss">.hero-blog { &__image { filter: brightness(0.29); }
&__title { margin: 0 0 1rem; color: map.get($map: $colors, $key: c-white); }
&__subtitle { margin: 0 0 2rem; font-size: var(--s-font-h3); width: 10em; color: map.get($map: $colors, $key: c-white); }}</style>2.2 Componente: LastBlogs.vue
Este componente muestra las últimas cinco entradas del blog. Se integra con NuxtLink para crear enlaces a las publicaciones. Los blogs están organizados por fecha de publicación, mostrando los más recientes primero.
<script setup lang="ts">import type { Blog } from "@/interfaces/api/blog";const BlogCard = defineAsyncComponent( () => import("@/components/ui/BlogCard.vue"));const TitleCommon = defineAsyncComponent( () => import("@/components/common/TitleCommon.vue"));
const { t } = useI18n();const [titlelastNews, titleStronglastNews] = [ t("news.last_news_title[0]"), t("news.last_news_title[1]"),];
interface Prop { response: Blog[];}
defineProps<Prop>();</script>
<template> <TitleCommon :text-align="'start'" class="last-blogs__title-common"> {{ titlelastNews }}<strong>{{ titleStronglastNews }}</strong> </TitleCommon> <section class="last-blogs__last"> <div class="last-blogs__div"> <NuxtLinkLocale class="last-blogs__card" v-for="blogItem in response.slice(0, 5)" :key="blogItem.id" :to="{ name: 'noticias-slug', params: { slug: blogItem.slug } }" > <BlogCard v-bind="blogItem" /> </NuxtLinkLocale> </div> </section></template>
<style scoped lang="scss">@import "./scss/mixinCards";.last-blogs { @include blog-cards();
&__last { width: fit-content; }
&__div { @include flex($wrap: wrap, $gap: 0.8em); }
&__card { transition: opacity 0.3s ease-in-out; &:first-child { border: 0.0625rem solid var(--c-primary); border-radius: var(--s-border-radius-button); } }}</style>2.3 Componente: OutstandingBlogs.vue
Este componente muestra un blog destacado con una imagen grande y un resumen. El blog destacado es el que tiene la fecha de publicación más reciente.
<script setup lang="ts">import type { Blog } from "@/interfaces/api/blog";const TextImageSection = defineAsyncComponent( () => import("@/components/common/TextImageSection.vue"));
interface OutstandingBlog { image: string; title: string;}
interface Props { outStandingBlog: OutstandingBlog | null; newsStand: Blog[] | null;}
defineProps<Props>();</script>
<template> <NuxtLinkLocale class="outstanding" v-if="newsStand" :to="{ name: 'noticias-slug', params: { slug: newsStand[0].slug } }" > <TextImageSection v-if="outStandingBlog" v-bind="outStandingBlog"> <section class="outstanding__blog-text" v-if="newsStand"> <h2>{{ newsStand[0].blog_title }}</h2> <p>{{ newsStand[0].outstanding_summary }}</p> </section> </TextImageSection> </NuxtLinkLocale></template>
<style scoped lang="scss">.outstanding { transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; &:hover { opacity: 0.75; transform: translateX(2em); }
&__blog-text { font-size: 1em; @include clampText(12); }}</style>2.4 Componente: NewsStand.vue
El componente NewsStand.vue se encarga de mostrar una cuadrícula con todas las entradas de blog disponibles, por orden de fecha de publicación.
<script setup lang="ts">import type { Blog } from "@/interfaces/api/blog";const BlogCard = defineAsyncComponent( () => import("@/components/ui/BlogCard.vue"));const TitleCommon = defineAsyncComponent( () => import("@/components/common/TitleCommon.vue"));
const { t } = useI18n();const [titleAllNews, titleAllNewsStrong] = [ t("news.all_news_title[0]"), t("news.all_news_title[1]"),];
interface Prop { newsStand: Blog[] | null;}
defineProps<Prop>();</script>
<template> <TitleCommon :text-align="'start'" class="news-stand__title-common"> {{ titleAllNews }}<strong>{{ titleAllNewsStrong }}</strong> </TitleCommon> <section class="news-stand"> <section class="news-stand__grid"> <NuxtLinkLocale class="news-stand__card" v-for="blogItem in newsStand" :key="blogItem.id" :to="{ name: 'noticias-slug', params: { slug: blogItem.slug } }" > <BlogCard v-bind="blogItem" /> </NuxtLinkLocale> </section> </section></template>
<style scoped lang="scss">@import "./scss/mixinCards";.news-stand { padding: 0 var(--s-padding-lateral); overflow: hidden;
@include responsive() { padding: 0 var(--s-padding-lateral-mobile); }
&__title-common { padding: 0 var(--s-padding-lateral);
@include responsive() { padding: 0 var(--s-padding-lateral-mobile); } }
&__grid { width: 100%; display: grid; grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); gap: 2em; place-items: center; }}</style>3. Configuración de las Páginas de Noticias
3.1 Página principal de Noticias: index.vue
Esta página lista todas las entradas del blog, incluyendo componentes como el héroe, blogs recientes y destacados.
<script setup lang="ts">import type { Blog } from "@/interfaces/api/blog";import { blogApiService } from "@/services/blogApiService";import LastBlogs from "@/components/blog/LastBlogs.vue";import NewsStand from "@/components/blog/NewsStand.vue";
const HeroBlog = defineAsyncComponent( () => import("@/components/blog/HeroBlogs.vue"));const OutstandingBlog = defineAsyncComponent( () => import("@/components/blog/OutstandingBlogs.vue"));
const response = ref<Blog[]>();
onBeforeMount(async () => { const { data } = await useServices<Blog[]>(blogApiService); response.value = data;});</script>
<template> <section class="container-blog"> <HeroBlog /> <LastBlogs v-if="response" :response="response" /> <OutstandingBlog v-if="response" :out-standing-blog="outStandingBlog" :news-stand="newsStand" /> <NewsStand v-if="response" :news-stand="newsStand" /> </section></template>
<style scoped lang="scss">.container-blog { overflow-x: hidden;}</style>3.2 Página de detalle del Blog: slug.vue
Esta página muestra los detalles de una entrada de blog específica, cargando el contenido dinámicamente según el slug.
<script setup lang="ts">import type { Blog } from "@/interfaces/api/blog";import { blogApiService } from "@/services/blogApiService";import TheHero from "@/components/common/TheHero.vue";
const route = useRoute();const slug = computed(() => route.params.slug);const response = ref<Blog[]>();
onBeforeMount(async () => { const { data } = await useServices<Blog[]>(blogApiService); response.value = data;});
const blog = computed(() => { if (!response.value) return null; return filterSingleBlog(slug.value.toString(), response.value);});</script>
<template> <section class="blog"> <TheHero> <template #background> <NuxtImg :src="blog?.blog_image?.url" :alt="blog?.blog_image?.alternativeText" class="blog__image" /> </template> <template #content> <h1 class="blog__title">{{ blog?.blog_title }}</h1> </template> </TheHero> <article class="blog__article" v-html="blog?.blog_content"></article> </section></template>
<style scoped lang="scss">.blog { padding: 0 var(--s-padding-lateral); &__article { max-width: 60rem; }}</style>4. Interfaces TS
4.1 Definición de Tipos para Blogs
Para mantener un código limpio y legible, es recomendable definir los tipos de datos que se utilizan en la aplicación. Aquí se muestra un ejemplo de cómo se pueden definir los tipos para las entradas de blog.
import type { Media, SEO, Locale } from "./common";
export interface Expert { readonly id: number; readonly image: Media; readonly locale: string; readonly name: string;}
export interface Blog { readonly blog_content: string; readonly blog_date: string | null; readonly blog_image?: Media; readonly blog_reading_time: string; readonly blog_title: string; readonly card_summary: string; readonly expert: Expert; readonly id: number; readonly locale: Locale; readonly localizations?: Blog[]; readonly outstanding_summary: string; readonly seo?: SEO; readonly slug: string;}5. Integración con Strapi
Ejemplo de cómo puedes configurar un servicio en Nuxt3 para hacer llamadas a la API de Strapi:
import { httpClient } from "./useFetchApi";import type { Blog } from "@/interfaces/api/blog";import type { LanguageCode } from "@/types/locale";import { LOCALE } from "@/constants/languages";
export const blogApiService = async (lang: LanguageCode) => { const params = [ { name: "populate[blogs][populate]", value: "*", }, { name: "populate[blog_image][populate]", value: "*", }, { name: "populate[localizations][populate]", value: "*", }, { name: "populate[expert][populate]", value: "*", }, { name: "pagination[limit]", value: "1000", }, { name: LOCALE, value: lang, }, ]; const { response } = await httpClient.get<Blog[]>("blogs", params); return { response };};Luego, este servicio se usa en las páginas de Nuxt para obtener datos de Strapi.
import { blogApiService } from '@/services/blogApiService'import type { Blog } from '@/interfaces/api/blog'
const response = ref<Blog[]>()
onBeforeMount(async () => { const { data } = await useServices<Blog[]>(blogApiService) response.value = data})El objeto response contiene los datos de las entradas de blog que se pueden utilizar en los componentes de Nuxt3.
6. composables
Los composables utilizados para este desarrollo son:
useFilterBlog.ts
7. Casos de Uso
Este desarrollo del blog ha sido diseñado teniendo en cuenta los siguientes casos de uso:
- Blog Destacado: Mostrar un blog destacado de forma prominente en la página principal.
- Últimos Blogs: Mostrar las entradas de blog más recientes.
- Listado de Blogs: Mostrar una lista de blogs en una cuadrícula organizada.
- Internacionalización (i18n): Los textos y enlaces están preparados para ser traducidos.
- Lazy Loading: Los componentes se cargan de forma asíncrona para optimizar el rendimiento de la página.
- Navegación Dinámica: Cada entrada de blog se puede acceder dinámicamente utilizando slugs en la URL.