Cómo refactorizar Astro para mejorar la mantenibilidad del código


En “Cómo crear un blog con Astro”, te contaba los primeros pasos para crear tu blog con Astro, y en “Cómo adaptar Astro a TypeScript” te decía cómo ajustar las configuraciones de Astro para usar TypeScript y sacarle el máximo partido (aunque Astro usa TypeScript de serie y te recomiendo que lo configures así desde el inicio).

Sin embargo…

La plantilla de base:

1. tiene código duplicado:

Por ejemplo, src/pages/index.astro, src/pages/blog/index.astro, y src/layouts/BlogPost.astro tienen el siguiente código en común:

---
import BaseHead from '@components/BaseHead.astro';
import Header from '@components/Header.astro';
import Footer from '@components/Footer.astro';
---

<!doctype html>
<html lang="en">
    <head>
        <BaseHead title={...} description={...} />
        ...
    </head>
    <body>
        <Header />
        <main>
            ...
        </main>
        <Footer />
    </body>
</html>

Si queremos hacer un cambio en este código, tendremos que hacerlo en los tres archivos… por ahora… porque, si añadimos más páginas “puras” o que usen layouts similares a BlogPost, la lista se alarga.

2. está pensada para un blog al uso:

Es decir, para tener algunas páginas “puras” (como la página de inicio, página “Sobre mí”, …), un archivo de artículos de blog y artículos de blog.

Podemos verlo en que el archivo de la página del blog está hecho específicamente para artículos de blog. ¿Qué pasa si quiero añadir otro tipo de publicaciones “seriadas” (por ejemplo, charlas que doy, eventos, …)?

Básicamente, duplicar el código y volver al punto 1.

¿Qué pasa si queremos algo más complejo? Ahora, veremos cómo refactorizar el código que genera para poder reutilizarlo con facilidad.

3. realizar cambios implica copiar código

¡Ojo aquí! Código es código y contenido es contenido. Es normal que tengamos que añadir contenido si queremos ampliar lo que hay en la web… pero ¿duplicar o copiar código?

Refactorizar componentes

Nombrar componentes

Si miras la carpeta src/components verás que aparecen componentes como BaseHead o FormattedDate… pero no hay ningún otro tipo de head o date. Además, en el caso de la fecha (date) asumimos que estará formateada para el usuario.

Por eso, voy a establecer convenciones de nombrado:

  1. Si no hay otro componente que realice una función similar, evitaremos generar un nombre compuesto.
  2. Si el componente hace referencia a una etiqueta HTML existente, lo llamaremos igual que la etiqueta, respetando el casing del lenguaje (en este caso Astro).

Con esto, vamos a hacer algunos cambios:

  1. src/components/BaseHead.astro pasará a ser src/components/Head.astro,
    1. todo lo que hay en el componente es el contenido correspondiente a la etiqueta head,
    2. para mantener la coherencia, necesitaremos hacer cambios en:
      1. src/layouts/BlogPost.astro,
      2. src/pages/blog/index.astro,
      3. y src/pages/index.astro,
  2. src/components/FormattedDate.astro pasará a ser src/components/Date.astro,
    1. no hay otro componente que represente fechas,
    2. para mantener la coherencia, necesitaremos hacer cambios en:
      1. src/layouts/BlogPost.astro,
      2. y src/pages/blog/index.astro,
  3. src/layouts/BlogPost.astro pasará a ser src/layouts/PageContent.astro,
    1. no solo se usa en los artículos de blog sino también en páginas como src/pages/about.astro,
    2. cambiamos el nombre a PageContent y no a Content porque ya existe un Content usado por Astro,
      1. es cierto que, al importar el componente con Astro, podemos ponerle el nombre que queramos, pero se trata de reducir la carga cognitiva al mínimo (es decir, intentar que haya que pensar lo menos posible),
    3. para mantener la coherencia, necesitaremos hacer cambios en:
      1. src/pages/about.astro,
      2. y src/pages/blog/[...slug].astro,

Cohesionar los componentes

Si te fijas en el código, ahora mismo nos encontramos con que la etiqueta head, en general, solo tiene el componente Head. Si hacemos que la etiqueta pase a estar en el componente, ganamos en legibilidad, puesto que pasamos de

<head>
	<Head />
	...
</head>

a

<Head>
	...
</Head>

Por un lado, necesitaremos hacer estos cambios en: 1. src/layouts/BlogPost.astro, 2. src/pages/blog/index.astro, 3. y src/pages/index.astro,

Por otro lado, esto implica que el componente Head debería tener, al menos, un slot definido y que, tras hacer los cambios, el componente Head (en src/components/Head.astro) se verá de la siguiente manera:

---
// Import the global.css file here so that it is included on
// all pages through the use of the <Head /> component.
import '@styles/global.css';

interface Props {
	title: string;
	description: string;
	image?: string;
}

const canonicalURL = new URL(Astro.url.pathname, Astro.site);

const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
---

<head>
	<!-- Global Metadata -->
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width,initial-scale=1" />
	<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
	<meta name="generator" content={Astro.generator} />

	<!-- Font preloads -->
	<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
	<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />

	<!-- Canonical URL -->
	<link rel="canonical" href={canonicalURL} />
	<slot />

	<!-- Primary Meta Tags -->
	<title>{title}</title>
	<meta name="title" content={title} />
	<meta name="description" content={description} />

	<!-- Open Graph / Facebook -->
	<meta property="og:type" content="website" />
	<meta property="og:url" content={Astro.url} />
	<meta property="og:title" content={title} />
	<meta property="og:description" content={description} />
	<meta property="og:image" content={new URL(image, Astro.url)} />

	<!-- Twitter -->
	<meta property="twitter:card" content="summary_large_image" />
	<meta property="twitter:url" content={Astro.url} />
	<meta property="twitter:title" content={title} />
	<meta property="twitter:description" content={description} />
	<meta property="twitter:image" content={new URL(image, Astro.url)} />
</head>

Centralizar las configuraciones

Ahora mismo, el menú principal de la cabecera y los menús sociales en cabecera y pie de página están escritos tal cual en los archivos.

Por eso, a la hora de hacer cualquier cambio, necesitaremos modificar los archivos copiando y adaptando el código. Esta práctica es propensa a errores así que vamos a refactorizar para evitarnos los fallos centralizando las configuraciones de menús en archivos externos.

Para ello:

  1. Instala ReactJS para Astro:
    yarn astro add react
    
  2. Crea el tipo MenuItem en un archivo específico que llamaremos src/typs/menus.d.ts. Un elemento de menú necesita, al menos, un texto que mostrar (label) y una ruta a la que ir (path). En nuestro caso, como también vamos a gestionar los menús sociales, tendremos un icono opcional. Esta definición debería ser algo como lo que sigue (pero siéntete libre de modificarlo cómo necesites):
    import { FC, SVGProps } from 'react';
    
    export type MenuItem = Readonly<{
        label: string;
        path: string;
        icon?: FC<SVGProps<SVGSVGElement>>;
    }>;
    

    Nota:

    Al definir el tipo MenuItem como Readonly estamos forzando la inmutabilidad del objeto. Es decir, podemos inicializarlo sin problemas pero, después de eso, cambiar sus valores lanzará un fallo en tiempo de transpilación. Con esto nos aseguramos de que el objeto no cambie en ningún momento.

  3. Crea el tipo Menu y añádelo al archivo src/types/menus.d.ts. Esta definición debería ser algo como lo que sigue (pero siéntete libre de modificarlo cómo necesites):
    export type Menu = MenuItem[];
    

    Nota:

    Aunque podríamos evitar esta definición (al final, solo estamos definiendo un array de MenuItem), cualquier cambio en esta definición implica cambiar importaciones. De esta manera, podemos evitar dicho cambio en un futuro, lo cual es importante si cada necesidad de cambio es una invitación a generar fallos.

  4. Crea una carpeta para guardar las configuraciones. En mi caso, es la carpeta src/config.
  5. Extrae los iconos a sus propios archivos en una subcarpeta de src/components (yo los he puesto en src/components/icons). El icono de Github debería verse así (y te servirá de ejemplo para el resto):
    import { type SVGProps } from 'react';
    
    const GithubIcon = (props: SVGProps<SVGSVGElement>) => (
        <svg
            viewBox="0 0 16 16"
            aria-hidden="true"
            width="32"
            height="32"
            {...props}
        >
            <path
                d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
                fill="currentColor"
            />
        </svg>
    );
    
    export default GithubIcon;
    
  6. Configura el acceso directo a estas dos nuevas rutas en el archivo tsconfig.json. Tras los cambios, debería verse como sigue:
    {
        "extends": "astro/tsconfigs/strict",
        "compilerOptions": {
            "baseUrl": "src/",
            "paths": {
                "@/*": ["./*"],
                "@components/*": ["components/*"],
                "@config/*": ["config/*"],
                "@icons/*": ["components/icons/*"],
                "@layouts/*": ["layouts/*"],
                "@styles/*": ["styles/*"]
            },
            "strictNullChecks": true
        }
    }
    
  7. Crea el archivo de configuración de los menús. En mi caso, es el archivo src/config/menus.config.ts.
  8. Extrae la configuración del menú de cabecera en este archivo. El código debería verse como sigue:
    import type { Menu } from '@/types/menus.d';
    
    export const mainMenu: Menu = [
        { label: 'Home', path: '/' },
        { label: 'Blog', path: '/blog' },
    ];
    
  9. Extrae ahora la configuración del menú social al archivo src/config/menus.config.ts. Debería verse como sigue:
    import type { Menu } from '@/types/menus.d';
    import GithubIcon from '@/components/icons/GithubIcon';
    
    export const mainMenu: Menu = [
        { label: 'Home', path: '/' },
        { label: 'Blog', path: '/blog' },
    ];
    
    export const socialMenu: Menu = [
        {
            label: "Go to Templates's GitHub repo",
            path: "https://github.com/borjalofe/astro-blog-template",
            icon: GithubIcon,
        },
    ];
    
  10. Refactoriza src/components/Header.astro para que use esta configuración. El código en src/components/Header.astro debería quedar de la siguiente manera:
    ---
    import HeaderLink from '@components/HeaderLink.astro';
    import { mainMenu, socialMenu } from '@config/menus.config';
    import { SITE_TITLE } from '@/consts';
    ---
    
    <header>
        <nav>
            <h2><a href="/">{SITE_TITLE}</a></h2>
            <div class="internal-links">
                {mainMenu.map(
                    ({label, path}) => <HeaderLink href={path}>{label}</HeaderLink>
                )}
            </div>
            <div class="social-links">
                {socialMenu.map(
                    ({icon: Icon, label, path}) => (
                        <a href={path} target="_blank">
                            <span class="sr-only">{label}</span>
                            {Icon && <Icon />}
                        </a>
                    )
                )}
            </div>
        </nav>
    </header>
    <style>
        ...
    </style>
    
  11. Refactoriza src/components/Footer.astro para que use esta configuración. El código en src/components/Footer.astro debería quedar de la siguiente manera:
    ---
    import { socialMenu } from '@config/menus.config';
    
    const today = new Date();
    ---
    
    <footer>
        &copy; {today.getFullYear()} Your name here. All rights reserved.
        <div class="social-links">
            {socialMenu.map(
                ({icon: Icon, label, path}) => (
                    <a href={path} target="_blank">
                        <span class="sr-only">{label}</span>
                        {Icon && <Icon />}
                    </a>
                )
            )}
        </div>
    </footer>
    <style>
        ...
    </style>
    

Con los cambios que acabamos de hacer:

  1. podemos añadir etiquetas al head simplemente modificando src/components/Head.astro (para etiquetas que afecten a toda la web) o usando el slot nombrado como “head” (para etiquetas que afectan a una página o layout concreto),
  2. podemos modificar los elementos de los menús simplemente cambiando el contenido del archivo src/config/menus.config.ts

Aún podemos hacer más por simplificarnos el trabajo futuro… pero eso queda para un próximo artículo.

Una pequeña trampa

Gracias por llegar hasta aquí. En próximos artículos, seguiré contando cómo crear una plantilla personalizada.

Aunque si no quieres crearla, puedes usar la mía (la que tendrás, más o menos, si sigues todos los pasos de todos los artículos de esta serie) con el siguiente comando:

yarn create astro -- --template borjalofe/astro-blog-template