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:
- Si no hay otro componente que realice una función similar, evitaremos generar un nombre compuesto.
- 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:
src/components/BaseHead.astro
pasará a sersrc/components/Head.astro
,- todo lo que hay en el componente es el contenido correspondiente a la etiqueta
head
, - para mantener la coherencia, necesitaremos hacer cambios en:
src/layouts/BlogPost.astro
,src/pages/blog/index.astro
,- y
src/pages/index.astro
,
- todo lo que hay en el componente es el contenido correspondiente a la etiqueta
src/components/FormattedDate.astro
pasará a sersrc/components/Date.astro
,- no hay otro componente que represente fechas,
- para mantener la coherencia, necesitaremos hacer cambios en:
src/layouts/BlogPost.astro
,- y
src/pages/blog/index.astro
,
src/layouts/BlogPost.astro
pasará a sersrc/layouts/PageContent.astro
,- no solo se usa en los artículos de blog sino también en páginas como
src/pages/about.astro
, - cambiamos el nombre a
PageContent
y no aContent
porque ya existe unContent
usado por Astro,- 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),
- para mantener la coherencia, necesitaremos hacer cambios en:
src/pages/about.astro
,- y
src/pages/blog/[...slug].astro
,
- no solo se usa en los artículos de blog sino también en páginas como
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:
- Instala ReactJS para Astro:
yarn astro add react
- Crea el tipo
MenuItem
en un archivo específico que llamaremossrc/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
comoReadonly
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. - Crea el tipo
Menu
y añádelo al archivosrc/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. - Crea una carpeta para guardar las configuraciones. En mi caso, es la carpeta
src/config
. - Extrae los iconos a sus propios archivos en una subcarpeta de
src/components
(yo los he puesto ensrc/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;
- 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 } }
- Crea el archivo de configuración de los menús. En mi caso, es el archivo
src/config/menus.config.ts
. - 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' }, ];
- 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, }, ];
- Refactoriza
src/components/Header.astro
para que use esta configuración. El código ensrc/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>
- Refactoriza
src/components/Footer.astro
para que use esta configuración. El código ensrc/components/Footer.astro
debería quedar de la siguiente manera:--- import { socialMenu } from '@config/menus.config'; const today = new Date(); --- <footer> © {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:
- podemos añadir etiquetas al
head
simplemente modificandosrc/components/Head.astro
(para etiquetas que afecten a toda la web) o usando elslot
nombrado como “head” (para etiquetas que afectan a una página o layout concreto), - 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