Dopo la prima pubblicazione del mio sito personale, sono passati cinque mesi durante i quali, tra impegni lavorativi e personali, mi sono ritagliato del tempo per migliorarlo ulteriormente.
Come tutti sappiamo, il tempo non è mai un alleato e, mentre la vita continua, anche la tecnologia evolve.
Infatti con l'arrivo di Next.js 15, ho deciso di aggiornare il mio sito web e di implementare quelle migliorie che, fino ad ora, non avevo avuto modo di realizzare.
Questo articolo si concentra sugli aggiornamenti delle tecnologie utilizzate e sulle nuove scelte stilistiche che ho voluto intraprendere. L’obiettivo non è solo quello di creare uno spazio dove raccogliere i miei progetti personali, ma anche quello di custodire e condividere i miei appunti più preziosi.
La scelta delle tecnologie è stata guidata principalmente dalla versione zero del progetto, mantenendo così l'adozione delle stesse tecnologie, aggiornandone le versioni e migliorando l’integrazione complessiva.
Lo stack include Next.js, Tailwind CSS, MDX e Typescript.
Next.js è un framework basato su React che semplifica la configurazione e ottimizza le prestazioni delle applicazioni web. Con la versione 15, sono state introdotte diverse novità, tra cui:
Tailwind CSS è un framework CSS basato su utility class, che semplifica la creazione di interfacce responsive e personalizzate.
Attualmente, sto utilizzando la versione 3 e ho valutato un aggiornamento alla versione 4, il quale richiederà una revisione sostanziale della configurazione e delle scelte stilistiche differenti, pertanto ho deciso di attendere un momento più propizio per l’upgrade.
TypeScript fornisce un sistema di tipi statici per JavaScript, migliorando la qualità e la manutenibilità del codice. Grazie a TypeScript, posso individuare errori durante la fase di scrittura, velocizzando lo sviluppo e riducendo la necessità di consultare continuamente la documentazione.
MDX consente di combinare componenti JSX con Markdown. Questa caratteristica è perfetta per creare articoli interattivi e progetti personalizzati in cui i componenti diventano parte integrante del contenuto.
Nella versione zero del sito, avevo scelto la libreria Contentlayer per la gestione dei file MDX all’interno di Next.js. Con Next.js 15, la gestione dei file MDX è stata migliorata e integrata nativamente, così ho optato per utilizzare le utility fornite dal framework, abbandonando Contentlayer, che non supportava ancora l’aggiornamento a Next.js 15 e presentava vari errori.
All’interno della directory catalog
ho inserito tutti i componenti da utilizzare nei file MDX, in modo da averli raccolti in un unico punto.
Nel file mdx-components.ts
, presente nella directory lib
, sono aggregati tutti i componenti in un unico oggetto, come mostrato di seguito:
import {
Code,
Filesystem,
Grid,
Aside,
H1,
H2,
H3,
H4,
H5,
Anchor,
Ordered,
Unordered,
Horizontal,
Image,
Blockquote,
Deleted,
Strong,
} from "@/components";
const MDXComponents = {
Code,
Filesystem,
Grid,
Aside,
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
hr: Horizontal,
a: Anchor,
ul: Unordered,
ol: Ordered,
strong: Strong,
Img: Image,
blockquote: Blockquote,
del: Deleted,
};
export default MDXComponents;
In questo file, ogni componente (come <H1>
, <Anchor>
, ecc.) viene mappato ad una chiave dell’oggetto MDXComponents per essere facilmente utilizzato all’interno dei contenuti MDX.
Per migliorare la gestione e la formattazione del contenuto MDX, ho utilizzato diverse librerie rehype:
Nel file rehypeOptions.ts
ho definito le opzioni e le funzionalità aggiuntive per la compilazione dei file MDX:
import { Options } from "rehype-pretty-code";
import { type Node } from "unist";
import { visit } from "unist-util-visit";
export function rehypeExtractHeadings(headings) {
return (tree) => {
visit(tree, "element", (node) => {
if (/^h[1-6]$/.test(node.tagName) && node.properties?.id) {
const text = node.children.map((child) => child.value || "").join("");
headings.push({
heading: parseInt(node.tagName[1]),
text,
slug: node.properties.id,
});
}
});
};
}
export function rehypePrettyCodeClasses() {
return (tree: Node) => {
visit(tree, "element", (node: ElementNode) => {
if (node.properties?.["data-rehype-pretty-code-figure"] !== undefined) {
node.properties.className = (node.properties.className || []).concat(
"code-block"
);
}
});
};
}
export const rehypePrettyCodeOptions: Partial<Options> = {
onVisitLine(node) {
if (node.children.length === 0) {
node.children = [{ type: "text", value: " " }];
}
},
onVisitTitle(node) {
if (node.children[0]?.type === "text") {
node.properties["data-rehype-pretty-code-title"] = node.children[0].value;
}
node.properties.className = ["code-title"];
},
onVisitHighlightedLine(node) {
node.properties.className = ["highlighted-line"];
},
};
Utilizzo la funzione compileMDX
di "next-mdx-remote/rsc"
per integrare i componenti definiti in MDXComponents
e le configurazioni rehype.
In questo modo, partendo dal contenuto MDX, ottengo direttamente un JSX pronto per il rendering.
import { compileMDX } from "next-mdx-remote/rsc";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypePrettyCode from "rehype-pretty-code";
import {
rehypeExtractHeadings,
rehypePrettyCodeClasses,
rehypePrettyCodeOptions,
} from "./rehypeOptions";
import { MDXComponents } from "@/lib";
export const parseMDX = async <T>(source: string, headings: Heading[] = []) => {
return compileMDX<T>({
source,
options: {
parseFrontmatter: true,
mdxOptions: {
rehypePlugins: [
[rehypeSlug],
[rehypeAutolinkHeadings, { behavior: "wrap" }],
[rehypePrettyCode, rehypePrettyCodeOptions],
[rehypePrettyCodeClasses],
[rehypeExtractHeadings, headings],
],
},
},
components: MDXComponents,
});
};
Per la gestione dei contenuti, ho sfruttato le API di Node.js per leggere i file MDX presenti nella directory /content
.
Ad esempio, il file lib/projects.ts
definisce le funzioni per ottenere l’elenco dei progetti e per recuperare i dettagli di un singolo progetto o di tutti i progetti:
In rootDirectory
viene definito il percorso in cui sono collocati tutti i file MDX relativi ai miei progetti. Successivamente, con l'utilizzo della libreria fs
recupero tutti i file presenti nella directory e, per ciascuno, estraggo le informazioni necessarie per eseguire il parse del file MDX.
import { promises as fs } from "fs";
import path from "path";
import { parseMDX, Heading, Project, MetaData } from "@/lib";
const rootDirectory = path.join(process.cwd(), "content", "projects");
export const getAllProjects = async (): Promise<
Omit<Project, "content" | "headings">[]
> => {
const filenames = await fs.readdir(rootDirectory);
const projectsPreviews = await Promise.all(
filenames.map(async (filename) => {
const slug = filename.replace(".mdx", "");
const projects = await getProjectBySlug(slug);
return { meta: projects.meta };
})
);
return projectsPreviews;
};
export const getProjectBySlug = async (slug: string): Promise<Project> => {
const filePath = path.join(rootDirectory, `${slug}.mdx`);
const fileContent = await fs.readFile(filePath, { encoding: "utf-8" });
const headings: Heading[] = [];
const { frontmatter, content } = await parseMDX<MetaData>(
fileContent,
headings
);
if (
!frontmatter.title ||
!frontmatter.description ||
!frontmatter.publishedAt ||
!frontmatter.status
) {
throw new Error(`Il file ${slug}.mdx ha un frontmatter non valido.`);
}
return { meta: { ...frontmatter, slug }, content, headings };
};
Con queste funzioni posso generare la pagina di presentazione dei progetti e, grazie a getProjectBySlug
,
recuperare i dettagli di un progetto specifico per la visualizzazione nella pagina dedicata.
Nella pagina che mostra l’elenco dei progetti, utilizzo getAllProjects
per recuperare i progetti
pubblicati e li ordino per data. Ogni progetto viene poi rappresentato tramite il componente <ArticlePreview>
:
import { notFound } from "next/navigation";
import { getAllProjects } from "@/lib/projects";
import { ArticlePreview } from "@/components";
export default async function Page() {
const projects = await getAllProjects();
const publishedProjects = projects
.filter((p) => p.meta.status === "published")
.sort(
(a, b) =>
Number(new Date(b.meta.publishedAt)) -
Number(new Date(a.meta.publishedAt))
);
if (!publishedProjects) {
notFound();
}
return (
<div className="mt-8 space-y-10">
{publishedProjects.map((project) => {
return <ArticlePreview key={project.meta.slug} meta={project.meta} />;
})}
</div>
);
}
La pagina dedicata ad un singolo progetto utilizza sia getProjectBySlug
per recuperare i dati, sia generateMetadata
per ottimizzare la SEO:
import { notFound } from "next/navigation";
import { Article } from "@/components";
import { getAllProjects, getProjectBySlug } from "@/lib/projects";
import { generateMeta } from "@/lib";
export async function generateStaticParams() {
const projects = await getAllProjects();
return projects
.filter((p) => p.meta.status === "published")
.map((p) => ({ slug: p.meta.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) {
notFound();
}
return generateMeta(project);
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) {
notFound();
}
return <Article article={project} />;
}
Per gli appunti, ho creato due tipologie di contenuti: post brevi (senza pagina dedicata) e articoli dettagliati. Nella pagina dedicata al blog recupero i post, filtro quelli pubblicati e li ordino per data.
A seconda del tipo di contenuto, utilizzo il componente <ArticlePreview>
o <Post>
:
import { ArticlePreview, Post, Sparkles } from "@/components";
import { getAllPosts } from "@/lib/blog";
import { MetaType } from "@/lib/types";
import { notFound } from "next/navigation";
export default async function Page() {
const posts = await getAllPosts();
const publishedArticles = posts
.filter((p) => p.meta.status === "published")
.sort(
(a, b) =>
Number(new Date(b.meta.publishedAt)) -
Number(new Date(a.meta.publishedAt))
);
if (!publishedArticles) {
notFound();
}
return (
<div className="mt-8 space-y-10">
{publishedArticles.length > 0 ? (
publishedArticles.map((post) =>
post.meta.type === MetaType.Article ? (
<ArticlePreview key={post.meta.slug} meta={post.meta} />
) : (
<Post key={post.meta.slug} post={post} />
)
)
) : (
<div className="text-center">
<Sparkles>
<span>Presto, in arrivo, nuovi appunti!</span>
</Sparkles>
</div>
)}
</div>
);
}
Il layout è strutturato in modo da mantenere una coerenza con la versione zero del progetto. Le pagine principali (come la home e la pagina dei contatti) condividono lo stesso layout globale, mentre le pagine dedicate ai progetti e agli appunti adottano un layout specifico.
Per l'ottimizzazione SEO, ho integrato robots.txt e sitemap.xml, generata dinamicamente con Next.js. Infatti NextJs permette di generare dinamicamente la sitemap per tutte le pagine del sito, garantendo che ogni nuovo progetto e articolo sia automaticamente aggiunto e indicizzato.
Ogni pagina è stata inserita nella sitemap, garentendo la generazione della sitempa in modo autoomatico. Visitando il sito online sarà possibile vedere la sitemap generata.
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const projects = await getAllProjects();
const publishedProjects = projects.filter(
(p) => p.meta.status === "published"
);
return publishedProjects.map(({ meta }) => ({
url: `https://marcodecarlo.com/projects/${meta.slug}`,
lastModified: new Date(meta.publishedAt),
}));
}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://marcodecarlo.com</loc>
<lastmod>2024-09-19T11:02:26.296Z</lastmod>
<changefreq>yearly</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://marcodecarlo.com/contact</loc>
<lastmod>2025-03-08T19:44:24.801Z</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://marcodecarlo.com/projects/antico-telaio</loc>
<lastmod>2024-06-01T00:00:00.000Z</lastmod>
</url>
<url>
<loc>https://marcodecarlo.com/projects/mio-sito-web</loc>
<lastmod>2024-10-31T00:00:00.000Z</lastmod>
</url>
<url>
<loc>https://marcodecarlo.com/projects/psicoterapeuta-blog</loc>
<lastmod>2024-09-01T00:00:00.000Z</lastmod>
</url>
<url>
<loc>https://marcodecarlo.com/projects/website-updates-new-features</loc>
<lastmod>2025-03-01T00:00:00.000Z</lastmod>
</url>
</urlset>
Grazie all'uso di generateMetadata()
vengono generati dinamicamente i metadati delle pagine.
Viene così personalizzato il titolo per ogni pagina e l'url canonico che è utile a livello SEO perchè aiuta i moti di ricerca a capire
quale versione di una pagina è considerata quella principale, evitando così errori di contenuti duplicati.
Ho aggiunto la possibilità di inserire un'immagine personalizzata utile per la condivisione.
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) {
notFound();
}
return generateMeta(project);
}
export const generateMeta = (project: Project): Metadata => {
const { meta } = project;
const url = `/projects/${meta.slug}`;
const images = meta.images?.length
? meta.images.map((img) => ({
url: img.url,
width: 1200,
height: 630,
alt: img.alt || "Marco De Carlo - Web Developer",
}))
: [
{
url: "/marco_decarlo.png",
width: 1200,
height: 630,
alt: "Marco De Carlo - Web Developer",
},
];
return {
title: `${meta.title} ⋅ Marco De Carlo`,
description: meta.description,
openGraph: {
title: `${meta.title} ⋅ Marco De Carlo`,
description: meta.description,
url: url,
siteName: "Marco De Carlo",
type: "website",
images: images,
},
twitter: {
card: "summary_large_image",
title: `${meta.title} ⋅ Marco De Carlo`,
description: meta.description,
creator: "@marco_dec",
images: images,
},
alternates: { canonical: url },
};
};
Infine, ho eseguito un’analisi delle prestazioni del sito web “marcodecarlo.com” utilizzando Lighthouse. I risultati confermano un’ottima ottimizzazione sotto diversi aspetti: prestazioni, accessibilità, SEO e best practices. Questi dati attestano l’efficacia degli interventi apportati, dimostrando come il sito risulti non solo veloce, ma anche ben strutturato e sicuro.
Dal confronto tra la versione zero e la nuova release emergono alcuni punti fondamentali:
Questo approccio mirato all’ottimizzazione ha permesso di mantenere alte le prestazioni complessive del sito, apportando miglioramenti strategici nelle aree che maggiormente influenzano l’esperienza dell’utente.
Vercel è una piattaforma di deployment e collaborazione per sviluppatori frontend, integrata con GitHub, che consente di mantenere un flusso di lavoro fluido tra sviluppo e rilascio. Nel mio caso, gestisco due branch principali:
Utilizzo una numerazione del tipo X.X.X-SNAPSHOT, dove:
Quando le modifiche vengono integrate in main, il suffisso -SNAPSHOT viene rimosso, segnalando che la versione è stabile e pronta per la distribuzione.
Attualmente, mentre scrivo, l'ultima versione è la v1.0.0, che potete trovare su GitHub: https://github.com/marcodecarlo/marcodecarlo/. Da questo momento in poi, ho deciso di chiudere la repository, e i futuri sviluppi non saranno più resi disponibili.
Questa decisione è stata presa per diverse ragioni, tra cui: