LogoCésar Alberca
- 10 minutos

Rediseño del Blog

Diseña Mejor Arquitectura Frontend
Patrones de Arquitectura Frontend complejos explicados de forma sencilla. Envíos recurrentes y directos a tu correo. Conviértete en un mejor Frontend Architect.
Respeto tu privacidad. Cancela en cualquier momento.

La newsletter está escrita en inglés.

Recientemente me convertí en Freelancer y Nómada Digital viajando por el mundo. Este hito solo podía celebrarse rediseñando mi blog.

No eres un verdadero Desarrollador Web si no rediseñas tu blog cada año.

Ser freelancer significa que no solo necesito ser técnicamente competente, sino que también debo pensar en cómo puedo comunicar eficazmente lo que ofrezco para que la gente quiera contratar mis servicios.

Una forma poderosa de transmitir tu mensaje —sea cual sea— es asegurarse de que el medio en el que se transmite tu mensaje sea lo suficientemente atractivo. El diseño es algo que los humanos hacemos con bastante frecuencia: diseñamos nuestra ropa, diseñamos cómo hablamos y, como diseñador web, diseñé y construí este mismo sitio que estás experimentando.

#Tecnologías

La tecnología necesita servir a un propósito. A veces —la mayoría de las veces, seamos honestos— los desarrolladores web nos enfocamos demasiado en las tecnologías y frameworks que utilizamos. Deberíamos hablar de ellos, estar orgullosos de lo que usamos, e incluso tal vez burlarnos de las elecciones de otros, pero solo hasta cierto punto. Aquí menciono algunas de las tecnologías que he usado, qué he aprendido de ellas y cómo me han permitido enfocarme en lo que realmente importa: el mensaje.

Un buen punto de partida es usar "starters". Yo usé el Next.js starter portfolioSe abre en una nueva pestaña. Me proporcionó algo de configuración inicial para SEO, así que no tuve que preocuparme por ello (por ahora). Por ejemplo, incluía sitemap.ts, robots.ts, rss/route.ts y og/route.ts.

Al usar un starter, lo que hago es generar la plantilla en una carpeta temporal y luego elijo qué copiar a mi proyecto. Esto asegura que no tome más de lo que necesito y que pueda revisar exactamente lo que quiero traer.

También configuré el proyecto como suelo hacerlo, con PrettierSe abre en una nueva pestaña, ESLintSe abre en una nueva pestaña, HuskySe abre en una nueva pestaña y Lint StagedSe abre en una nueva pestaña. Cómo configuro los proyectos en los que trabajo definitivamente podría ser otro post en algún momento.

Tomé una decisión consciente —tal vez también debatible— de no profundizar demasiado en buenas prácticas, testing y arquitectura. ¿Por qué? Porque quiero crear una serie en el blog donde pueda mejorar la base de código bit a byte. Pero bueno, no es que haya programado la cosa con los ojos cerrados (lo cual podría ser un desafío interesante, para ser honesto). Puedes consultar el código fuente aquíSe abre en una nueva pestaña en el momento de publicar este post. Creo que está bastante bien.

En lo que decidí enfocarme en cambio fue en crear un diseño limpio con un gran enfoque en el contenido y algunas animaciones sutiles que harían el sitio b·e·l·l·o. Aquí tienes algunas:

Animaciones

Texto con acento
Uno
Fondo (¡más hover!)
Fondo con imagen (¡todavía hover!)
Tarjeta 3D (lo adivinaste, ¡hover!)

Siempre estoy en busca de inspiración. Ver cómo otros han abordado problemas similares me reafirma que no estoy solo en mi búsqueda de mi solución. Algunos lugares geniales de los que robé ideas busqué inspiración fueron CodropsSe abre en una nueva pestaña y AwwwardsSe abre en una nueva pestaña.

#MDX

Como puedes ver, todos los componentes que estoy desarrollando para el sitio web pueden ser reutilizados en las publicaciones del blog, y eso es gracias a MDXSe abre en una nueva pestaña. MDX es como Markdown con Kombucha (es decir, superpotente). ¿Por qué? Porque en MDX puedes importar componentes de React, lo cual es una locura.

**Foo** <Background> <div className="p-xl">Bar</div> </Background> > Baz

Lo cual renderizará esto:

Foo

Bar

Baz

Genial, ¿verdad?

Next.js viene con soporte para MDX usando algunas librerías adicionales. Comencé configurando el proyecto siguiendo la guía de MDX de Next.jsSe abre en una nueva pestaña, que en algún momento sugiere usar next-mdx-remoteSe abre en una nueva pestaña. Me pareció un poco engorroso y noté que proporcionaba algunas funcionalidades que realmente no necesito (YAGNI).

Después de buscar soluciones más simples, me topé con este fantástico postSe abre en una nueva pestaña, que mostraba cómo simplificar la configuración usando solo next-mdxSe abre en una nueva pestaña.

Lo único que todavía necesito resolver es cómo generar metadatos compatibles con SEO para cada artículo y charla. Pero eso será otro POST.

La estructura de las publicaciones es bastante simple. Solo le decimos a Next.js: "por favor, renderiza archivos .mdx cuando se te pida":

import withMDX from '@next/mdx' /** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], } export default withMDX()(nextConfig)

Y luego es cuestión de especificar en qué ruta se renderizaría la publicación.

src/ └── app/ └── blog/ └── (posts)/ └── blog-redesign/ └── page.mdx

Como queremos personalizar los componentes que MDX renderiza, podemos crear un archivo bajo src llamado mdx-components para hacerlo:

import type { MDXComponents } from 'mdx/types' import Link from 'next/link' import { type ComponentProps, createElement, type LinkHTMLAttributes, type PropsWithChildren, type ReactNode, } from 'react' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import dark from 'react-syntax-highlighter/dist/esm/styles/prism/synthwave84' import { cn } from '@/lib/utils' function CustomLink(props: LinkHTMLAttributes<HTMLAnchorElement> & PropsWithChildren<{ href: string }>) { let href = props.href if (href.startsWith('/')) { return <Link {...props}>{props.children}</Link> } if (href.startsWith('#')) { return <a {...props} /> } return <a target="_blank" rel="noopener noreferrer" {...props} /> } function Code({ children, ...props }: { children: string; className: string }) { const className = props?.className ?? '' const match = /language-(\w+)/.exec(className ?? '') return match ? ( <SyntaxHighlighter {...props} language={match[1]} PreTag="div" style={dark} codeTagProps={{ style: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', }, }} > {children} </SyntaxHighlighter> ) : ( <code {...props} className={cn(className, 'bg-muted px-[4px] py-[2px] mx-[2px] rounded not-prose text-base font-mono')} > {children} </code> ) } function slugify(str: string) { return str .toString() .toLowerCase() .trim() // Elimina espacios en blanco de ambos extremos de una cadena .replace(/\s+/g, '-') // Reemplaza espacios con - .replace(/&/g, '-and-') // Reemplaza & con 'and' .replace(/[^\w\-]+/g, '') // Elimina todos los caracteres que no sean palabras excepto - .replace(/\-\-+/g, '-') // Reemplaza múltiples - con un solo - } function createHeading(level: number) { const Heading = ({ children }: { children: string }) => { const slug = slugify(children) return createElement( `h${level}`, { id: slug }, [ createElement('a', { href: `#${slug}`, key: `link-${slug}`, className: 'anchor', }), ], children, ) } Heading.displayName = `Heading${level}` return Heading } const customComponents: ComponentProps<any>['components'] = { h1: createHeading(1), h2: createHeading(2), h3: createHeading(3), h4: createHeading(4), h5: createHeading(5), h6: createHeading(6), a: CustomLink, code: Code, pre: ({ children }: { children: ReactNode }) => <pre className="p-0 font-mono">{children}</pre>, } export function useMDXComponents(components: MDXComponents): MDXComponents { return { ...customComponents, ...components, } }

Cada artículo también debería tener el mismo diseño que el sitio web. Para lograr esto, puedes exportar una función por defecto que use un componente personalizado. Me hubiera gustado que esto estuviera integrado en el sistema de diseño de Next.js. ¿Tal vez en el futuro?

import { PostLayout } from '@/features/posts/delivery/post-layout' # Contenido de mi blog export default function Page({ children }) { return <PostLayout slug="blog-redesign">{children}</PostLayout> }

El componente PostLayout tiene el siguiente contenido:

import { Page } from '@/core/components/page/page' import type { ReactNode } from 'react' import { PostPage } from '@/features/posts/delivery/post.page' import type { PostMetadata } from '@/post-metadata' export async function PostLayout({ children, slug }: { children: ReactNode; slug: string }) { const { metadata } = (await import(`./(posts)/${slug}/page.mdx`)) as { metadata: PostMetadata } return ( <Page top> <PostPage metadata={metadata} slug={slug}> {children} </PostPage> </Page> ) }

Hay algunos metadatos en cada artículo que se utilizan para generar los metadatos de SEO. Estos metadatos se exportan como una const. Por ejemplo, los metadatos de este mismo artículo son:

export const metadata = { slug: 'blog-redesign', title: 'Blog Redesign', date: '2024-08-25', image: 'blog-redesign/medusa.jpg', categories: ['software-development', 'branding', 'design'], readTime: 15, summary: 'Después de dejar mi trabajo, decidí rediseñar mi blog. Esta es la historia de cómo lo hice.', }

Esto elimina la necesidad de añadir front-matter y un parser posterior. Lo más simple es mejor.

#TaildwindCSS

Por si te lo perdiste, hay algo de código que inicialmente podría parecer aberrante. Si es así, es porque aún no has probado TailwindCSSSe abre en una nueva pestaña:

<code {...props} className={cn(className, 'bg-muted px-[4px] py-[2px] mx-[2px] rounded not-prose text-base font-mono')}> {children} </code>

Para mí, el CSS es parte de la arquitectura frontend, y es algo que no paso por alto ni odio. Quería incorporar TailwindCSS para ver si es escalable y proporciona un buen framework para el desarrollo. Conclusión: lo hace.

Sin embargo, hay dos cambios que hice:

  1. Usar unidades de espaciado con nombre en lugar de números arbitrarios.
  2. Usar nombres orientados al tema en lugar de colores específicos.

Es importante para mí abstraer las unidades de espaciado. No quiero usar números como p-2, m-8 o w-4. En su lugar, prefiero usar p-s, m-m o w-xl. De esta manera, puedo cambiar las unidades de espaciado en un solo lugar y se reflejará en todo el proyecto.

Lo mismo ocurre con los colores. Normalmente no verás colores específicos en el código (a menos que quiera usar invariablemente un color en particular). En su lugar, verás text-primary, bg-secondary, border-destructive, etc. Este enfoque proporciona más flexibilidad y mantenibilidad al proyecto, especialmente si quieres cambiar el color primario en el futuro.

import type { Config } from 'tailwindcss' const config = { theme: { extend: { spacing: { xxs: '8px', xs: '12px', s: '16px', m: '24px', l: '32px', xl: '48px', xxl: '56px', }, colors: { border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))', }, destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))', }, muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))', }, accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))', }, popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))', }, card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))', }, }, }, }, } satisfies Config export default config

#ShadcnUI

ShadcnUI introduce un concepto que inicialmente podría hacernos estremecer de miedo: copiar y pegar código. Lo hace generando código para tus componentes. Pero ¿por qué querría eso? Bueno, sirve como base para componentes ligeros y, a partir de ahí, puedo personalizarlos tanto como quiera. El código es mío después de todo.

Cada vez que necesito un nuevo componente, me dirijo a su documentaciónSe abre en una nueva pestaña y encuentro el componente que necesito. Luego, lo genero usando la CLI:

npx shadcn-ui@latest add button

El botón se generará en src/components/ui/button.tsx y se verá algo como esto:

import * as React from 'react' import { Slot } from '@radix-ui/react-slot' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean } const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button' return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> }, ) Button.displayName = 'Button' export { Button, buttonVariants }

Como puedes ver, usa TailwindCSS y RadixUI internamente para hacer todo el trabajo pesado. class-variance-authority es una librería que facilita la creación de variantes de un componente.

Otra razón por la que elegí ShadcnUI es su integración con la nueva herramienta de IA de Vercel para generar piezas de interfaz de usuario: v0Se abre en una nueva pestaña. ¿Está el diseño web ya condenado? Bueno, ¿cuándo no lo estuvo?

#¿Qué sigue?

Necesito quiero volver a escribir en el blog, cubriendo desde temas técnicos hasta historias más personales. Quiero compartir mi proyectos, mis experiencias como Nómada Digital e incluso la receta que uso para hacer mi desodorante. Mi objetivo es crear un sitio que me encante visitar y disfrute leyendo. Y si tú también lo haces, bueno, ¡eso sería increíble!

Gracias por leer este post
¿Quieres apoyar ideas que merecen ser compartidas? Apoyar este blog me compra tiempo para escribir.
Más tiempo significa más valor para ti.