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:
/components/ui/blog/ - HeroBlogs.vue - LastBlogs.vue - NewsStand.vue - OutstandingBlogs.vue - BlogCard.vue
/pages/blogs/ - index.vue - [slug].vue
/layouts/pages/ - blogs.vue - blogSlug.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">import { PLACEHOLDER } from "@/constants/placeholders";import type { BlogsLocales } from "@/interfaces/locales/verticals/shared/blogs";
const { data: blogsLocale } = await useLocales<BlogsLocales>("blogs");</script>
<template> <SectionsHeroTheHero> <template #background> <NuxtImg :placeholder="PLACEHOLDER.blogs" :src="PLACEHOLDER.blogs" :alt="blogsLocale.image_generic_alt" :title="blogsLocale.image_generic_alt" class="hero-blog__image" /> </template> </SectionsHeroTheHero></template>
<style scoped lang="scss">.hero-blog { &__title { color: var(--c-white); text-align: center; margin: 0.5em 0; }
&__subtitle { margin: 0 0 2rem; font-size: var(--s-font-h3);
color: var(--c-white); text-align: center; }}</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/shared/blog";
interface OutstandingBlog { image: string; image_alt: string; title: string;}
interface Props { outStandingBlog: OutstandingBlog | null; newsStand: Blog[] | null; blogText?: string;}
defineProps<Props>();</script><template> <NuxtLinkLocale class="outstanding" v-if="newsStand" :to="{ name: 'blogs-slug', params: { slug: newsStand[0].slug } }" > <LazyUiTextImageSection v-if="outStandingBlog" v-bind="outStandingBlog"> <section class="outstanding__blog-text" v-if="newsStand"> <h2>{{ newsStand[0].title }}</h2> <p>{{ newsStand[0].outstandingSummary }}</p> </section> </LazyUiTextImageSection> </NuxtLinkLocale></template>
<style scoped lang="scss">.outstanding { margin-bottom: 5em; 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.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/shared/blog";
interface OutstandingBlog { image: string; image_alt: string; title: string;}
interface Props { outStandingBlog: OutstandingBlog | null; newsStand: Blog[] | null; blogText?: string;}
defineProps<Props>();</script><template> <NuxtLinkLocale class="outstanding" v-if="newsStand" :to="{ name: 'blogs-slug', params: { slug: newsStand[0].slug } }" > <LazyUiTextImageSection v-if="outStandingBlog" v-bind="outStandingBlog"> <section class="outstanding__blog-text" v-if="newsStand"> <h2>{{ newsStand[0].title }}</h2> <p>{{ newsStand[0].outstandingSummary }}</p> </section> </LazyUiTextImageSection> </NuxtLinkLocale></template>
<style scoped lang="scss">.outstanding { margin-bottom: 5em; 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/shared/blog";import type { BlogsLocales } from "@/interfaces/locales/verticals/shared/blogs";
const { data: blogsLocale } = await useLocales<BlogsLocales>("blogs");interface Prop { newsStand: Blog[] | null;}
defineProps<Prop>();</script><template> <LazyUiTheTitle :text-align="'start'" class="news-stand__title-common" >{{ blogsLocale.all_news_title }}<strong>{{ blogsLocale.all_news_strong }}</strong></LazyUiTheTitle > <section class="news-stand"> <section class="news-stand__grid"> <NuxtLinkLocale class="news-stand__card" v-for="blogItem in newsStand" :key="blogItem.id" :to="{ name: 'blogs-slug', params: { slug: blogItem.slug } }" > <LazyUiBlogCard v-bind="blogItem" /> </NuxtLinkLocale> </section> </section></template>
<style scoped lang="scss">.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); margin: 3em 0;
@include responsive() { padding: 0 var(--s-padding-lateral-mobile); } }
&__last { padding: 0 var(--s-padding-lateral); margin: 0 0 4em; }
&__card { transition: scale 0.3s ease-in-out; &:hover { scale: 1.03; } }
&__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 Blogs
3.1 Página principal de Blogs: index.vue
Esta pagina se encarga en Base a la estructura del proyecto a llamar al Layout correspondiente del index de la Pagina Blog, de esta manera
<script setup lang="ts">import { chooseLayoutPage } from "@/core/plugins";const layout = chooseLayoutPage({ page: "blogs" });</script><template> <NuxtLayout :name="layout"></NuxtLayout></template>Si nos dirigimos a la ruta /layouts/pages/blogs.vue encontraremos la pagina index de Blogs.
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/shared/blog";import type { BlogsLocales } from "@/interfaces/locales/verticals/shared/blogs";
const { data: blogsLocale } = await useLocales<BlogsLocales>("blogs");
const data = await useServices<Blog[]>("blogs");const response = ref<Blog[]>();response.value = data;
const { filterOutstandingBlog } = useBlogsFilters();
const outStandingBlog = computed(() => { if (!response.value) return null; // eslint-disable-next-line @typescript-eslint/no-unused-vars const [filteredBlog, ..._] = filterOutstandingBlog(response.value); return { image: filteredBlog.imageCard!.url, image_alt: filteredBlog.imageCard!.alternativeText ?? blogsLocale.image_generic_alt, title: filteredBlog.title, };});
const newsStand = computed(() => { if (!response.value) return null; return filterOutstandingBlog(response.value);});</script>
<template> <section class="container-blog"> <UiBlogHeroBlogs />
<UiBlogLastBlogs v-if="response?.length" :response="response" />
<UiBlogOutstandingBlogs v-if="response?.length" :out-standing-blog="outStandingBlog" :news-stand="newsStand" />
<LazyUiBlogNewsStand v-if="response?.length" :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 pagina se encarga en Base a la estructura del proyecto a llamar al Layout correspondiente del index de la Pagina BlogSlug, de esta manera
<script setup lang="ts">import { chooseLayoutPage } from "@/core/plugins";const layout = chooseLayoutPage({ page: "blog-slug" });</script><template> <NuxtLayout :name="layout"></NuxtLayout></template>Si nos dirigimos a la ruta /layouts/pages/blogSlug.vue encontraremos la pagina Slug de Blogs.
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/shared/blog";import { PLACEHOLDER } from "@/constants/placeholders";
const route = useRoute();const slug = computed(() => route.params.slug);
const response = ref<Blog>();const data = await useServices<Blog, { slug: string }>("blogBySlug", { slug: slug.value.toString(),});response.value = data;
const blog = computed(() => { if (!response.value) return null; return response.value;});
const blogText = computed(() => { if (!blog.value) return; const { sanitizedDescription } = useSanitizeDescription(blog.value.content); return sanitizedDescription.value;});</script>
<template> <section class="blog"> <SectionsHeroTheHero> <template #background> <NuxtImg :src="blog?.imageInternal?.url || PLACEHOLDER.banner" :alt="blog?.imageInternal?.alternativeText || blog?.title" /> </template> </SectionsHeroTheHero> <h1 class="blog__title">{{ blog?.title }}</h1> <article class="blog__time"> <time>{{ blog?.date }}</time> </article> <article class="blog__article" v-html="blogText"></article> </section></template>
<style scoped lang="scss">.blog { @include responsive() { padding: 0 var(--s-padding-lateral-mobile); }
&__title { color: var(--c-white); text-align: center; margin: 1.2em 0 0; padding: 0 var(--s-padding-lateral); @include responsive() { width: 100%; font-size: var(--s-font-h3-mobile); } }
&__time { text-align: center; margin: 1em 0 0; }
&__article { margin: 4em auto; max-width: 60rem; &:deep(p) { margin: 0.5em 0; font-family: var(--f-font-thin); font-size: var(--s-font-p); line-height: 2.67rem; }
&:deep(h2) { margin: 0.5em 0; font-size: var(--s-font-h3); font-family: var(--f-font-light); color: var(--c-primary); }
&:deep(img), &:deep(video) { margin: 1em 0; }
&:deep(video) { cursor: pointer; }
&:deep(a) { color: var(--c-primary); font-size: var(--f-font-p);
&:hover { text-decoration: underline; } } } &:deep(blockquote > p) { font-style: italic; display: flex; margin: 1em 0;
&:before { content: "“"; font-size: 4rem; margin-right: 0.2em; font-weight: bold; color: var(--c-primary); } }
&:deep(ul) { margin: 1em 0; padding-left: 2em; }
&:deep(ol) { padding-left: 2em; } &:deep(li) { margin: 0.5em 0;
&:before { content: "•"; color: var(--c-primary); margin-right: 0.5em; } }}</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 } from "@/types/shared/media";import type { Expert } from "@/interfaces/api/shared/experts";export interface Blog { id: number; documentId: string; title: string; date: string; author: Expert; slug: string; content: string; imageCard: Media; imageInternal: Media; vertical: string[]; summary: string; outstandingSummary: string;}5. Integración con Strapi
- Este codigo se utiliza para recuperar todos los Blogs guardados en el Strapi.
import type { Blog } from "@/interfaces/api/shared/blog";import type { FilterParams } from "@/types/shared/filterParams";import type { LanguageCode } from "@/types/locale";
export const getBlogs = async ( lang: LanguageCode, { vertical }: FilterParams) => { const { find } = useStrapi(); const { data: response } = await find<Blog>("blogs", { locale: lang, filters: { vertical: { $containsi: vertical }, }, fields: [ "id", "title", "slug", "vertical", "summary", "outstandingSummary", "date", ], populate: { imageCard: { fields: ["url", "alternativeText"] }, author: { fields: ["name"], populate: { imageExpert: { fields: ["url", "alternativeText"] }, }, }, }, });
return response;};- Este codigo se utiliza para recuperar todos los Blogs guardados por Slug en el Strapi.
export const getBlogBySlug = async ( lang: LanguageCode, { slug, vertical }: FilterParams) => { const { find } = useStrapi(); const { data } = await find<Blog>("blogs", { locale: lang, filters: { slug, vertical: { $containsi: vertical }, }, fields: ["id", "title", "slug", "vertical", "content", "date"], populate: { imageInternal: { fields: ["url", "alternativeText"], }, }, });
const response = data[0];
return response;};Luego, este servicio se usa en las páginas de Nuxt para obtener datos de la petición que hace Strapi (en las vistas).
import type { Blog } from '@/interfaces/api/shared/blog'
const data = await useServices<Blog[]>('blogs')const response = ref<Blog[]>()response.value = dataEl 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.