aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/PortfolioProject.astro84
-rw-r--r--src/components/TechnologyBadge.astro (renamed from src/partials/TechnologyBadge.astro)81
-rw-r--r--src/content.config.ts4
-rw-r--r--src/data/blog/libertad-prensa-españa.md13
-rw-r--r--src/data/portfolio/dynamic-tourist-info.mdx9
-rw-r--r--src/data/portfolio/mientreno.mdx4
-rw-r--r--src/data/portfolio/order-extractor.mdx22
-rw-r--r--src/data/portfolio/qr-ponteareas.mdx13
-rw-r--r--src/data/portfolio/qr-touro.mdx13
-rw-r--r--src/data/portfolio/vigo-360.mdx2
-rw-r--r--src/i18n/es.json14
-rw-r--r--src/layouts/BlogListLayout.astro325
-rw-r--r--src/layouts/Layout.astro5
-rw-r--r--src/layouts/PortfolioItemLayout.astro77
-rw-r--r--src/layouts/PortfolioPageLayout.astro110
-rw-r--r--src/pages/blog/[id].astro10
-rw-r--r--src/pages/blog/index.astro317
-rw-r--r--src/pages/portfolio/[id].astro57
-rw-r--r--src/pages/portfolio/index.astro2
-rw-r--r--src/partials/Footer.astro2
-rw-r--r--src/partials/Header.astro12
21 files changed, 669 insertions, 507 deletions
diff --git a/src/components/PortfolioProject.astro b/src/components/PortfolioProject.astro
new file mode 100644
index 0000000..b3483e9
--- /dev/null
+++ b/src/components/PortfolioProject.astro
@@ -0,0 +1,84 @@
+---
+import { Icon } from "astro-icon/components";
+import TechnologyBadge from "./TechnologyBadge.astro";
+
+interface Props {
+ title: string;
+ summary: string;
+ tags: string[];
+
+ detailsLink?: string;
+ githubLink?: string;
+ onlineLink?: string;
+}
+
+const { title, summary, tags, detailsLink, githubLink, onlineLink } =
+ Astro.props;
+---
+
+<article>
+ <h3>{title}</h3>
+
+ <p>{summary}</p>
+
+ <div>
+ {detailsLink && <a href={detailsLink}>
+ <Icon name="tabler:info-circle"/>
+ Detalles
+ </a>}
+
+ {githubLink && <a href={githubLink}>
+ <Icon name="tabler:brand-github"/>
+ GitHub
+ </a>}
+
+ {onlineLink && <a href={onlineLink}>
+ <Icon name="tabler:link"/>
+ En línea
+ </a>}
+ </div>
+
+ <div>
+ {tags.map(tag => (
+ <TechnologyBadge code={tag} />
+ ))}
+ </div>
+</article>
+
+<style lang="scss">
+ @use "../../styles/variables" as *;
+
+ h3, p, div, a {
+ margin: 0;
+ }
+
+ article {
+ display: flex;
+ flex-direction: column;
+ justify-content: start;
+ gap: 0.75rem;
+
+ padding: 1.5rem 1rem;
+ border-radius: 0.5rem;
+
+ background-color: white;
+ border: 1px solid $accent;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ a {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--text);
+ text-decoration: none;
+ }
+
+ div *:not(:last-child) {
+ margin-inline-end: 0.5rem;
+ }
+
+ div:nth-last-child(1) {
+ margin-block-start: 0.5rem;;
+ }
+</style>
diff --git a/src/partials/TechnologyBadge.astro b/src/components/TechnologyBadge.astro
index c5e170e..83d0d40 100644
--- a/src/partials/TechnologyBadge.astro
+++ b/src/components/TechnologyBadge.astro
@@ -1,87 +1,84 @@
---
+import Icon from "node_modules/astro-icon/components/Icon.astro";
+
interface Technology {
name: string;
colour: string;
text?: "light" | "dark";
- icon: string;
+ icon?: string;
}
export const technologies: { [key: string]: Technology } = {
java: {
name: "Java",
colour: "#e76f00",
- icon: "java",
+ icon: "coffee"
},
dotnet: {
name: ".NET",
colour: "#512bd4",
- icon: "dotnet",
+ icon: "brand-c-sharp",
},
go: {
name: "Go",
colour: "#00add8",
- icon: "go",
+ icon: "brand-golang",
},
mysql: {
name: "MySQL",
colour: "#3a75b0",
- icon: "mysql",
- },
- mongodb: {
- name: "MongoDB",
- colour: "#4db33d",
- icon: "mongodb",
- },
- sqlserver: {
- name: "SQL Server",
- colour: "#cc2927",
- icon: "sqlserver",
+ icon: "brand-mysql",
},
php: {
name: "PHP",
colour: "#8892be",
- icon: "php",
+ icon: "brand-php",
},
python: {
name: "Python",
colour: "#306998",
- icon: "python",
- },
- javascript: {
- name: "JavaScript",
- colour: "#ffe70b",
- text: "dark",
- icon: "javascript",
},
typescript: {
name: "TypeScript",
colour: "#007acc",
- icon: "typescript",
+ icon: "brand-typescript",
},
azure: {
name: "Azure",
colour: "#0089d6",
- icon: "azure",
+ icon: "brand-azure",
},
- linux: {
- name: "Linux",
- colour: "#010101",
- icon: "linux",
+ ubuntu: {
+ name: "Ubuntu",
+ colour: "#E95420",
+ icon: "brand-ubuntu",
},
windows: {
name: "Windows",
colour: "#0078d6",
- icon: "windows",
+ icon: "brand-windows",
},
astro: {
name: "Astro",
colour: "#3d50f5",
- icon: "astro",
+ icon: "brand-astro",
+ },
+ wordpress: {
+ name: "WordPress",
+ colour: "#21759b",
+ icon: "brand-wordpress",
},
- rabbitmq: {
- name: "RabbitMQ",
- colour: "#ff6600",
- icon: "rabbitmq",
+ github: {
+ name: "GitHub",
+ colour: "#181717",
+ text: "light",
+ icon: "brand-github",
+ },
+ web: {
+ name: "Web",
+ colour: "#f7df1e",
+ text: "dark",
+ icon: "world",
},
};
@@ -93,21 +90,33 @@ interface Props {
}
const { code, size } = Astro.props;
+if (!(code in technologies)) {
+ throw new Error(`Technology code "${code}" is not defined in technologies.`);
+}
const tech = technologies[code] as Technology;
---
<span class={`pill-${size ?? "small"} text-${tech.text ?? "light"}`}>
+ {
+ tech.icon && (
+ <Icon name={`tabler:${tech.icon}`} />
+ )
+ }
{tech.name}
</span>
<style define:vars={{ colour: tech.colour }}>
span {
- display: inline-block;
+ /*display: inline-block;*/
background-color: var(--colour);
font-weight: bold;
text-transform: uppercase;
border-radius: 0.5em;
padding: 0.5em 1em;
+
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
}
.text-dark {
diff --git a/src/content.config.ts b/src/content.config.ts
index a0391a8..f3d7755 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -17,6 +17,10 @@ const portfolio = defineCollection({
title: z.string(),
description: z.string(),
technologies: z.array(z.string()),
+ githubLink: z.string().url().optional(),
+ onlineLink: z.string().url().optional(),
+ demoLink: z.string().url().optional(),
+ images: z.array(z.string()).default([])
}),
});
diff --git a/src/data/blog/libertad-prensa-españa.md b/src/data/blog/libertad-prensa-españa.md
index 6dc95e6..350e32d 100644
--- a/src/data/blog/libertad-prensa-españa.md
+++ b/src/data/blog/libertad-prensa-españa.md
@@ -1,13 +1,22 @@
---
title: "En España sí hay libertad de prensa, por eso se puede decir lo contrario tranquilamente"
description: "La libertad de prensa en España es un derecho fundamental, y hay quien afirma no tenerlo... mientras lo ejerce."
-publishedAt: 2025-04-30
+publishedAt: 2025-06-05
tags: ["politica", "derecho", "españa"]
---
Estoy harto de leer y escuchar a gente diciendo que "en España no hay libertad de prensa" y que "todos los medios están comprados por el Gobierno", y nada más lejos de la realidad. De hecho, quienes afirman esto justamente lo hacen desde sus medios de comunicación (sean o no prensa, aunque no sea tan relevante en la era de las redes sociales), y lo hacen sin recibir ningún tipo de represalia. ¿No es eso libertad de prensa?
-En España tenemos medios públicos, como Radio Televisión Española y Radio Nacional (ambas RTVE) y autonómicos (como la TVG de Galicia, TV3 en Catalunya, EITB en el País Vasco, etc.), y medios privados, como El País, El Mundo, ABC, La Vanguardia, El Confidencial, elDiario.es, etc. Todos ellos tienen libertad para informar y opinar sobre lo que quieran, y no hay ningún tipo de censura previa.
+En España tenemos medios públicos: estatales (RTVE y RNE), autonómicos (como la TVG de Galicia, TV3 en Catalunya, ETB en el País Vasco, etc.) e incluso locales. y medios privados, como El País, ABC, La Vanguardia, El Confidencial, elDiario.es, etc. Todos ellos tienen libertad para informar y opinar sobre lo que quieran, y no hay ningún tipo de censura previa.
De hecho, incluso tenemos medios que se dedican a criticar al Gobierno y a los partidos políticos, disfrazando opiniones sesgadas como "información", o distribuyendo bulos y noticias falsas (como se dedica a hacer habitualmente OKDiario, por ejemplo) sin que nadie les haya censurado, únicamente multado por calumnias o injurias. Y eso es libertad de prensa.
+¿Cómo se puede hablar de falta de libertad de prensa donde siguen operando desde hace años medios conocidos por su burda desinformación y oposición al Gobierno como OKDiario, Libertad Digital, La Razón, The Objective y otros? O mismo televisiones, sean públicas o privadas, como Telecinco, Antena 3, El Toro TV (aka Intereconomía), Telemadrid o _La Telegaita_ (como denominamos los gallegos a la TVG), donde los trabajadores llevan [años denunciando la manipulación informativa](https://www.eldiario.es/galicia/huelga-indefinida-tvg-sube-peldano-convocatoria-gran-manifestacion-domingo-1-diciembre_1_11781704.html) forzada por el Gobierno gallego del PP.
+
+Muchas veces se confunden las palabras _libertad_ e _impunidad_. La libertad de prensa significa que tienes derecho a publicar libremente tus opiniones, noticias factuales o incluso bulos sin que nadie te censure antes de que lo hagas. Pero eso no significa que puedas hacer lo que quieras sin consecuencias. Si publicas algo falso o calumnioso, puedes ser demandado y condenado por ello. Eso es lo que ha pasado con algunos medios que han sido multados o condenados por injurias y calumnias, pero no se les ha impedido publicar sus contenidos.
+
+Donde sí había censura y falta de libertad de prensa era en la época de Franco, la que algunos parecen añorar, donde no se podía criticar al régimen sin arriesgarse a la cárcel o a la muerte. O en la época de la Transición, donde los medios tenían que autocensurarse para no ser cerrados o sancionados. Pero hoy en día, por suerte, eso ya no ocurre en España.
+
+Y si esto no es así ¿cómo es posible que no hayan cerrado el canal de Telegram de _Alvise_ o pseudomedios como The Objective (apodado _The Ojete_ de manera acertada por el ministro Óscar Puente) o "Estado de Alarma" con pseudoperiodistas como Javier Negre, Bertrand Ndongo o Vito Quiles.
+
+Dejemos de victimizarnos por todo. No todo es censura, y no todo es persecución. Aunque en realidad esto sea un paso más de la vergonzosa estrategia de "el que pueda hacer, que haga" que se ha convertido en la norma en la política española.
diff --git a/src/data/portfolio/dynamic-tourist-info.mdx b/src/data/portfolio/dynamic-tourist-info.mdx
deleted file mode 100644
index 677f0f6..0000000
--- a/src/data/portfolio/dynamic-tourist-info.mdx
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: "Punto de información turística en móviles"
-description: "Creación de un sitio web dinámico con PHP adaptado a móviles y accesible mediante QR en localizaciones físicas"
-technologies: ["php"]
----
-
-Creación de un sitio web dinámico con PHP adaptado a móviles y accesible mediante QR en localizaciones físicas. Cada página muestra datos de la ubicación correspondiente en tres idiomas (castellano, gallego e inglés), una galería de imágenes en 360º con la biblioteca PanoLens y vídeos incrustados de YouTube.
-
-Desasrrollado entre 2020 y 2021, en colaboración con [Kendra](https://kendra.es/). \ No newline at end of file
diff --git a/src/data/portfolio/mientreno.mdx b/src/data/portfolio/mientreno.mdx
index a7d8fab..79a40c6 100644
--- a/src/data/portfolio/mientreno.mdx
+++ b/src/data/portfolio/mientreno.mdx
@@ -1,7 +1,7 @@
---
title: "MiEntreno"
description: "Creación de un sitio web dinámico con PHP adaptado a móviles y accesible mediante QR en localizaciones físicas"
-technologies: ["dotnet", "sqlserver", "azure", "rabbitmq"]
+technologies: ["dotnet", "azure"]
---
Aplicación web para la gestión de entrenamientos deportivos, con una interfaz sencilla y fácil de usar. Desarrollado con ASP.NET Core, Razor Pages y SQL Server, y alojada en Azure App Service.
@@ -14,4 +14,4 @@ Además, el tiempo de desarrollo era muy limitado, al tratarse del proyecto fina
También, fue un "campo de pruebas" para mí, ya que fue el primer proyecto serio que desarrollé con ASP.NET Core, y donde también quise introducir el uso de emailing transaccional, colas de mensajes y otros conceptos más avanzados que no había utilizado antes en una aplicación real.
-El código fuente se puede encontrar en [GitHub](https://github.com/arielcostas/mientreno) bajo la licencia GNU Affero GPL v3.0. \ No newline at end of file
+El código fuente se puede encontrar en [GitHub](https://github.com/arielcostas/mientreno) bajo la licencia GNU Affero GPL v3.0.
diff --git a/src/data/portfolio/order-extractor.mdx b/src/data/portfolio/order-extractor.mdx
index d0a2ea6..dfdbd2e 100644
--- a/src/data/portfolio/order-extractor.mdx
+++ b/src/data/portfolio/order-extractor.mdx
@@ -4,8 +4,24 @@ description: "Extractor de pedidos de compra online para generar informes de ven
technologies: ["java", "windows"]
---
-Implementación de una aplicación de escritorio que extrae los datos sobre los pedidos on-line de diversas plataformas (como WooCommerce, Amazon y Ebay) para almacenar de forma local y centralizada. Además, cruza estos datos con la base de datos de productos en almacén y sus precios de coste, para generar informes de Excel sobre los ingresos y costes por cada pedido, así como calcular la rentabilidad de estos.
+Implementación de una aplicación de escritorio que extrae los datos sobre los pedidos on-line
+de diversas plataformas (como WooCommerce, Amazon y Ebay) para almacenar de forma local y
+centralizada. Además, cruza estos datos con la base de datos de productos en almacén y sus
+precios de coste, para generar informes de Excel sobre los ingresos y costes por cada pedido,
+así como calcular la rentabilidad de estos.
-Los principales problemas enfrentados en este proyecto fueron la diversidad de formatos de los datos de entrada y la necesidad de mantener la aplicación actualizada con los cambios en las plataformas de venta online; además de no contar con SDKs oficiales para estas plataformas, teniendo que implementar llamadas HTTP a las API públicas, a veces con autenticación compleja o firma de peticiones (como AWS Signature V4).
+Los principales problemas enfrentados en este proyecto fueron la diversidad de formatos de los
+datos de entrada y la necesidad de mantener la aplicación actualizada con los cambios en las
+plataformas de venta online; además de no contar con SDKs oficiales para estas plataformas,
+teniendo que implementar llamadas HTTP a las API públicas, a veces con autenticación compleja
+o firma de peticiones (como AWS Signature V4).
-Por otra parte, está la distribución, instalación y actualización de la aplicación en los equipos del cliente, teniendo que generar un instalador MSI firmado a partir del Java compilado y _shaded_ con sus dependencias mediante maven, y `jpackage` para generar el ejecutable nativo de Windows. \ No newline at end of file
+Por otra parte, está la distribución, instalación y actualización de la aplicación en los
+equipos del cliente, teniendo que generar un instalador MSI firmado a partir del Java compilado
+y _shaded_ con sus dependencias mediante maven, y `jpackage` para generar el ejecutable nativo
+de Windows.
+
+Trabajo inicial realizado en 2023 para un cliente de España, con mantenimiento hasta la actualidad.
+En 2025 se comenzó un proyecto de migración a una aplicación cloud con más funcionalidades y que
+soluciona dificultades como la sincronización de datos entre equipos y la generación de nuevos tipos
+de informes.
diff --git a/src/data/portfolio/qr-ponteareas.mdx b/src/data/portfolio/qr-ponteareas.mdx
new file mode 100644
index 0000000..61449f3
--- /dev/null
+++ b/src/data/portfolio/qr-ponteareas.mdx
@@ -0,0 +1,13 @@
+---
+title: "Museo a ceo aberto de Ponteareas"
+description: "Creación de un sitio web dinámico con PHP adaptado a móviles y accesible mediante QR en localizaciones físicas"
+technologies: ["php", "web"]
+demoLink: "https://museocorpus.ponteareas.gal/?place=01-bugallal"
+---
+
+Creación de un sitio web dinámico con PHP adaptado a móviles y accesible mediante
+QR en localizaciones físicas. Cada página muestra datos de la ubicación correspondiente
+en tres idiomas (castellano, gallego e inglés), una galería de imágenes en 360º con
+la biblioteca PanoLens y vídeos incrustados de YouTube.
+
+Desarrollado en 2021 en colaboración con [Kendra](https://kendra.es/).
diff --git a/src/data/portfolio/qr-touro.mdx b/src/data/portfolio/qr-touro.mdx
new file mode 100644
index 0000000..63a770e
--- /dev/null
+++ b/src/data/portfolio/qr-touro.mdx
@@ -0,0 +1,13 @@
+---
+title: "Punto de información turística en móviles"
+description: "Creación de un sitio web dinámico con PHP adaptado a móviles y accesible mediante QR en localizaciones físicas"
+technologies: ["typescript", "web"]
+demoLink: "https://www.concellodetouro.com/qr-carteis/petroglifo.html"
+---
+
+Desarrollo de un generador estático en TypeScript para información turística del Concello de
+Touro, A Coruña, con un sitio web dinámico adaptado a móviles. El sitio se accede mediante códigos QR
+en localizaciones físicas, permitiendo a los usuarios obtener información sobre el patrimonio cultural
+y natural de la zona.
+
+Desasrrollado en 2020, en colaboración con [Kendra](https://kendra.es/).
diff --git a/src/data/portfolio/vigo-360.mdx b/src/data/portfolio/vigo-360.mdx
index de42b5f..ddbaed2 100644
--- a/src/data/portfolio/vigo-360.mdx
+++ b/src/data/portfolio/vigo-360.mdx
@@ -1,7 +1,7 @@
---
title: "Vigo 360"
description: "Desarrollo y publicación de un blog sobre Vigo"
-technologies: ["go", "mysql", "linux"]
+technologies: ["go", "mysql", "ubuntu"]
---
Blog sobre Vigo y su entorno, orientado principalmente a hablar de movilidad y toponimia. Desarrollado en Go, con base de datos MySQL y desplegado sobre infraestructura propia.
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 6ad3bc4..b96b669 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -71,19 +71,7 @@
"intro": "En esta sección encontrarás una lista de los proyectos en los que he trabajado, tanto para terceros como propios. Si quieres más información sobre alguno de ellos, no dudes en <a href=\"/contact\">contactar conmigo</a>.",
"freelanceTitle": "Proyectos para terceros (freelance)",
"freelanceDesc": "He realizado desarrollos de proyectos para terceros por encargo, siendo los más destacados los siguientes:",
- "orderExtractorTitle": "Extractor de pedidos compra online",
- "orderExtractorDesc": "Aplicación de escritorio que extrae los datos sobre los pedidos on-line de diversas plataformas (como WooCommerce, Amazon y Ebay). <a href=\"/portfolio/order-extractor\">Más información</a>.",
- "touristInfoTitle": "Punto de información turística en móviles",
- "touristInfoDesc": "Aplicación web de información turística, con QR, contenido en 360º e incrustado de YouTube. <a href=\"/portfolio/dynamic-tourist-info\">Más información</a>.",
- "wpConsultingTitle": "Consultoría WordPress",
- "wpConsultingDesc": "Trabajos de mantenimiento, optimización y migración de sitios web WordPress y tiendas online WooCommerce. <a href=\"/portfolio/wp-consulting\">Más información</a>. <a href=\"/contact\">Contactar</a>.",
"ownProjectsTitle": "Proyectos propios",
- "ownProjectsDesc": "Además, tengo varios proyectos propios que he desarrollado en mi tiempo, estando algunos de ellos en activo, y como código abierto.",
- "personalWebTitle": "Web personal",
- "personalWebDesc": "Desarrollado con Astro, un generador de sitios web estáticos que permite escribir contenido en Markdown y publicar en la web con un rendimiento excelente. Desplegado sobre Azure Static Web Apps.",
- "mientrenoTitle": "MiEntreno (proyecto fin de ciclo)",
- "mientrenoDesc": "Aplicación web para la gestión de entrenamientos deportivos, con una interfaz sencilla y fácil de usar. Desarrollado con ASP.NET Core, Razor Pages y SQL Server. <a href=\"/portfolio/mientreno\">Más información</a>. <a href=\"https://github.com/arielcostas/mientreno\">Código fuente</a>.",
- "vigo360Title": "Vigo 360",
- "vigo360Desc": "Blog sobre Vigo y su entorno, orientado principalmente a hablar de movilidad y toponimia. Desarrollado en Go, con base de datos MySQL y desplegado sobre VPS administrado por mí mismo. <a href=\"/portfolio/vigo-360\">Más información</a>. <a href=\"https://github.com/arielcostas/vigo360\">Código fuente</a>. <a href=\"https://vigo360.es\">Web</a>."
+ "ownProjectsDesc": "Además, tengo varios proyectos propios que he desarrollado en mi tiempo, estando algunos de ellos en activo, y como código abierto."
}
}
diff --git a/src/layouts/BlogListLayout.astro b/src/layouts/BlogListLayout.astro
new file mode 100644
index 0000000..4ad72ea
--- /dev/null
+++ b/src/layouts/BlogListLayout.astro
@@ -0,0 +1,325 @@
+---
+import { getCollection } from "astro:content";
+import Layout from "@/layouts/Layout.astro";
+
+const blogCollection = (await getCollection("blog")).sort((a, b) => {
+ return b.data.publishedAt.getTime() - a.data.publishedAt.getTime();
+});
+
+// Agrupar artículos por fecha
+const groupedPosts = blogCollection.reduce(
+ (acc: Record<string, any[]>, post) => {
+ const year = post.data.publishedAt.getFullYear();
+ const month = post.data.publishedAt.getMonth() + 1;
+ const key = `${year}-${month}`;
+ if (!acc[key]) {
+ acc[key] = [];
+ }
+ acc[key].push(post);
+ return acc;
+ },
+ {},
+);
+
+// Colección de todas las etiquetas únicas
+const allTags = [...new Set(blogCollection.flatMap(post => post.data.tags || []))].sort();
+
+function humaniseDate(date: Date) {
+ const result = date.toLocaleDateString("es-ES", {
+ month: "long",
+ year: "numeric",
+ });
+ return result.charAt(0).toUpperCase() + result.slice(1);
+}
+
+const schema = {
+ "@context": "https://schema.org",
+ "@type": "Blog",
+ headline: "Blog de Ariel Costas",
+ description:
+ "En este blog encontrarás artículos sobre desarrollo, tecnología y otras temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones son mías, y no representan a ninguna empresa o institución.",
+ publisher: {
+ "@type": "Person",
+ name: "Ariel Costas Guerrero",
+ url: "https://www.costas.dev",
+ },
+ author: {
+ "@type": "Person",
+ name: "Ariel Costas Guerrero",
+ url: "https://www.costas.dev",
+ },
+};
+---
+
+<Layout
+ title="Blog"
+ description="Artículos sobre desarrollo, tecnología y otras temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones son mías, y no representan a ninguna empresa o institución."
+>
+ <script
+ is:inline
+ type="application/ld+json"
+ slot="head-jsonld"
+ set:html={JSON.stringify(schema)}
+ />
+
+ <h1>Blog de Ariel Costas</h1>
+
+ <p>
+ En este blog encontrarás artículos sobre desarrollo, tecnología y otras
+ temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones
+ son mías, y no representan a ninguna empresa o institución.
+ </p>
+
+ {allTags.length > 0 && (
+ <div class="tags-container">
+ <h2>Etiquetas</h2>
+ <div class="tag-filter">
+ <button class="tag-button active" data-tag="all">Todas</button>
+ {allTags.map((tag) => (
+ <button class="tag-button" data-tag={tag}>{tag}</button>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <div id="blog-posts">
+ {
+ Object.entries(groupedPosts).map(([key, posts]) => (
+ <section class="post-section" data-date={key}>
+ <h2>{humaniseDate(new Date(key))}</h2>
+ <ul>
+ {posts.map((post) => {
+ const postTags = post.data.tags || [];
+ const tagsAttribute = postTags.join(',');
+ return (
+ <li class="post-item" data-tags={tagsAttribute}>
+ <a href={`/blog/${post.id}`}>{post.data.title}</a>
+ {postTags.length > 0 && (
+ <ul class="post-tags">
+ {postTags.map((tag: string) => (
+ <li>
+ <button class="tag-link" data-tag={tag}>
+ {tag}
+ </button>
+ </li>
+ ))}
+ </ul>
+ )}
+ </li>
+ );
+ })}
+ </ul>
+ </section>
+ ))
+ }
+ </div>
+
+ <script>
+ document.addEventListener('DOMContentLoaded', () => {
+ const tagButtons = document.querySelectorAll('.tag-button');
+ const tagLinks = document.querySelectorAll('.tag-link');
+ const postItems = document.querySelectorAll('.post-item');
+ const postSections = document.querySelectorAll('.post-section');
+
+ function filterByTag(tag: string) {
+ postItems.forEach(item => {
+ (item as HTMLElement).style.display = '';
+ });
+ postSections.forEach(section => {
+ (section as HTMLElement).style.display = '';
+ });
+
+ if (tag !== 'all') {
+ postItems.forEach(item => {
+ const itemEl = item as HTMLElement;
+ const tagsAttr = itemEl.dataset.tags || '';
+ const itemTags = tagsAttr ? tagsAttr.split(',') : [];
+ if (!itemTags.includes(tag)) {
+ itemEl.style.display = 'none';
+ }
+ });
+
+ postSections.forEach(section => {
+ const items = section.querySelectorAll('.post-item');
+ let allHidden = true;
+
+ items.forEach(item => {
+ if ((item as HTMLElement).style.display !== 'none') {
+ allHidden = false;
+ }
+ });
+
+ if (allHidden) {
+ (section as HTMLElement).style.display = 'none';
+ }
+ });
+ }
+
+ tagButtons.forEach(button => {
+ if ((button as HTMLElement).dataset.tag === tag) {
+ button.classList.add('active');
+ } else {
+ button.classList.remove('active');
+ }
+ });
+
+ if (tag === 'all') {
+ history.replaceState(null, document.title, window.location.pathname);
+ } else {
+ history.replaceState(null, document.title, `?tag=${encodeURIComponent(tag)}`);
+ }
+ }
+
+ tagButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const tag = (button as HTMLElement).dataset.tag;
+ const isCurrentlyActive = button.classList.contains('active');
+
+ if (tag && isCurrentlyActive && tag !== 'all') {
+ // If the clicked tag is already active, switch back to "all"
+ filterByTag('all');
+ } else if (tag) {
+ // Otherwise apply the tag filter
+ filterByTag(tag);
+ }
+ });
+ });
+
+ tagLinks.forEach(link => {
+ link.addEventListener('click', (e) => {
+ e.preventDefault();
+ const tag = (link as HTMLElement).dataset.tag;
+
+ // Check if this tag is already active
+ const isCurrentlyActive = Array.from(tagButtons).some(button =>
+ (button as HTMLElement).dataset.tag === tag && button.classList.contains('active')
+ );
+
+ if (tag && isCurrentlyActive) {
+ // If the clicked tag is already active, switch back to "all"
+ filterByTag('all');
+ } else if (tag) {
+ // Otherwise apply the tag filter
+ filterByTag(tag);
+ }
+
+ const tagsContainer = document.querySelector('.tags-container');
+ if (tagsContainer) {
+ window.scrollTo({
+ top: (tagsContainer as HTMLElement).offsetTop - 20,
+ });
+ }
+ });
+ });
+
+ // Verificar si hay un parámetro de consulta para filtrar
+ const urlParams = new URLSearchParams(window.location.search);
+ const tagParam = urlParams.get('tag');
+
+ if (tagParam) {
+ const tagExists = Array.from(tagButtons).some(button =>
+ (button as HTMLElement).dataset.tag === tagParam
+ );
+ if (tagExists) {
+ filterByTag(tagParam);
+ }
+ }
+ });
+ </script>
+</Layout>
+
+<style lang="scss">
+ @use "../../styles/variables" as v;
+ @use "sass:color";
+
+ .tags-container {
+ margin-bottom: 2rem;
+ }
+
+ .tag-filter {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1.5rem;
+ }
+
+ .tag-button {
+ padding: 0.25rem 0.6rem;
+ background-color: v.$light;
+ color: v.$accent;
+ border: 1px solid v.$accent;
+ border-radius: 1.5rem;
+ font-size: 0.85rem;
+ font-family: v.$monoFontStack;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: v.$shadow;
+
+ &:hover {
+ background-color: color.adjust(v.$accent, $lightness: 45%);
+ color: v.$accentDark;
+ transform: translateY(-1px);
+ }
+
+ &.active {
+ background-color: v.$accent;
+ color: v.$lightAlt;
+ border-color: v.$accentDark;
+ }
+ }
+
+ .post-tags {
+ display: inline-flex;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ gap: 0.25rem;
+ margin-left: 0.5rem;
+ }
+
+ .post-tags li {
+ display: inline;
+ }
+
+ .tag-link {
+ display: inline-block;
+ padding: 0.1rem 0.4rem;
+ background-color: color.adjust(v.$background, $lightness: -3%);
+ color: v.$accentDark;
+ border: none;
+ border-radius: 1rem;
+ font-size: 0.75rem;
+ font-family: v.$monoFontStack;
+ text-decoration: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: color.adjust(v.$accent, $lightness: 45%);
+ color: v.$accentDark;
+ transform: translateY(-1px);
+ }
+ }
+
+ /* Efecto de transición para el filtrado */
+ .post-item {
+ transition: all 0.3s ease;
+ }
+
+ .post-section {
+ transition: opacity 0.3s ease;
+ }
+
+ #blog-posts {
+ min-height: 200px;
+ }
+
+ /* Mejora el aspecto de los enlaces de posts */
+ .post-item {
+ margin-bottom: 0.5rem;
+
+ a {
+ font-weight: 500;
+ }
+ }
+</style>
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 976e8a3..0df9191 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -1,6 +1,5 @@
---
-import "@fontsource/beiruti";
-import "@fontsource-variable/sen";
+import "@fontsource-variable/onest";
import Header from "../partials/Header.astro";
import Footer from "../partials/Footer.astro";
@@ -111,7 +110,7 @@ const { title, description, empty } = Astro.props;
align-self: center;
- max-width: 82ch;
+ max-width: min(1000px, 90vw);
font-size: 1.2rem;
padding-block-end: 3rem;
diff --git a/src/layouts/PortfolioItemLayout.astro b/src/layouts/PortfolioItemLayout.astro
new file mode 100644
index 0000000..95715cb
--- /dev/null
+++ b/src/layouts/PortfolioItemLayout.astro
@@ -0,0 +1,77 @@
+---
+import Layout from "@/layouts/Layout.astro";
+import { render } from "astro:content";
+import TechnologyBadge from "@/components/TechnologyBadge.astro";
+import type { InferEntrySchema } from "astro:content";
+import { Icon } from "astro-icon/components";
+
+interface Props {
+ entry: any;
+}
+
+const { entry } = Astro.props;
+const data = entry.data as InferEntrySchema<"portfolio">;
+const { Content } = await render(entry);
+---
+
+<Layout title={data.title} description={data.description}>
+ <a id="link-back" href="/portfolio">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ class="w-6 h-6 inline-block mr-2"
+ >
+ <polyline points="15 18 9 12 15 6"></polyline>
+ </svg>
+ Volver al portfolio
+ </a>
+
+ <h1>{data.title}</h1>
+
+ <Content />
+
+ <div>
+ {data.githubLink && <a href={data.githubLink}>
+ <Icon name="tabler:brand-github"/>
+ GitHub
+ </a>}
+
+ {data.onlineLink && <a href={data.onlineLink} target="_blank" rel="noopener noreferrer">
+ <Icon name="tabler:link"/>
+ En línea
+ </a>}
+
+ {data.demoLink && <a href={data.demoLink} target="_blank" rel="noopener noreferrer">
+ <Icon name="tabler:link"/>
+ Demo
+ </a>}
+ </div>
+
+ <h2>Tecnologías utilizadas</h2>
+
+ {
+ data.technologies.map((technology: string) => (
+ <TechnologyBadge size="small" code={technology} />
+ ))
+ }
+</Layout>
+
+<style>
+ a#link-back {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ text-decoration: none;
+ text-transform: uppercase;
+ transition: color 0.2s ease-in-out;
+ }
+
+ a#link-back svg {
+ height: 1em;
+ }
+</style>
diff --git a/src/layouts/PortfolioPageLayout.astro b/src/layouts/PortfolioPageLayout.astro
index c2d2b38..e213c10 100644
--- a/src/layouts/PortfolioPageLayout.astro
+++ b/src/layouts/PortfolioPageLayout.astro
@@ -2,7 +2,7 @@
import t from "../i18n/es.json";
import Layout from "./Layout.astro";
-import TechnologyBadge from "../partials/TechnologyBadge.astro";
+import PortfolioProject from "@/components/PortfolioProject.astro";
const schema = {
"@context": "https://schema.org",
@@ -29,31 +29,33 @@ const schema = {
<p>{t.portfolioPage.freelanceDesc}</p>
<section>
- <article>
- <h3>{t.portfolioPage.orderExtractorTitle}</h3>
+ <PortfolioProject
+ title="Extractor de pedidos venta online"
+ summary="Aplicación de escritorio que extrae los datos sobre los pedidos on-line de diversas plataformas (como WooCommerce, Amazon y Ebay)."
+ tags={["java", "windows"]}
+ detailsLink="/portfolio/order-extractor"
+ />
- <p set:html={t.portfolioPage.orderExtractorDesc} />
+ <PortfolioProject
+ title="Museo a ceo aberto de Ponteareas"
+ summary="Desarrollo de visualización de contenido turístico para el Concello de Ponteareas. Incluye contenido en 360º e incrustación de vídeos de YouTube. Realizado en 2021."
+ tags={["php", "web"]}
+ detailsLink="/portfolio/qr-ponteareas"
+ />
- <TechnologyBadge code="java" />
- <TechnologyBadge code="windows" />
- </article>
+ <PortfolioProject
+ title="QR Touro turístico"
+ summary="Desarrollo de un generador estático en TypeScript para información turística del Concello de Touro. Realizado en 2020."
+ tags={["typescript", "web"]}
+ detailsLink="/portfolio/qr-touro"
+ />
- <article>
- <h3>{t.portfolioPage.touristInfoTitle}</h3>
-
- <p set:html={t.portfolioPage.touristInfoDesc} />
-
- <TechnologyBadge code="php" />
- <TechnologyBadge code="mysql" />
- </article>
-
- <article>
- <h3>{t.portfolioPage.wpConsultingTitle}</h3>
-
- <p set:html={t.portfolioPage.wpConsultingDesc} />
-
- <TechnologyBadge code="php" />
- </article>
+ <PortfolioProject
+ title="Consultoría WordPress"
+ summary="Trabajos de mantenimiento, optimización y migración de sitios web WordPress y tiendas online WooCommerce."
+ tags={["php", "wordpress"]}
+ detailsLink="/portfolio/wp-consulting"
+ />
</section>
<h2>{t.portfolioPage.ownProjectsTitle}</h2>
@@ -61,52 +63,36 @@ const schema = {
<p>{t.portfolioPage.ownProjectsDesc}</p>
<section>
- <article>
- <h3>{t.portfolioPage.personalWebTitle}</h3>
-
- <p>{t.portfolioPage.personalWebDesc}</p>
-
- <TechnologyBadge code="astro" />
- <TechnologyBadge code="azure" />
- </article>
-
- <article>
- <h3>{t.portfolioPage.mientrenoTitle}</h3>
+ <PortfolioProject
+ title="Web personal"
+ summary="Desarrollado con Astro, un generador de sitios web estáticos que permite escribir contenido en Markdown y publicar en la web con un rendimiento excelente. Desplegado via GitHub Actions en mi servidor."
+ tags={["astro", "github", "ubuntu"]}
+ githubLink="https://github.com/arielcostas/costasdev"
+ />
- <p set:html={t.portfolioPage.mientrenoDesc} />
+ <PortfolioProject
+ title="MiEntreno (proyecto fin de ciclo)"
+ summary="Aplicación web para la gestión de entrenamientos deportivos, con una interfaz sencilla y fácil de usar. Desarrollado con ASP.NET Core, Razor Pages y SQL Server."
+ tags={["dotnet", "azure"]}
+ githubLink="https://github.com/arielcostas/mientreno"
+ detailsLink="/portfolio/mientreno"
+ />
- <TechnologyBadge code="dotnet" />
- <TechnologyBadge code="sqlserver" />
- <TechnologyBadge code="azure" />
- <TechnologyBadge code="rabbitmq" />
- </article>
-
- <article>
- <h3>{t.portfolioPage.vigo360Title}</h3>
-
- <p set:html={t.portfolioPage.vigo360Desc} />
-
- <TechnologyBadge code="go" />
- <TechnologyBadge code="mysql" />
- <TechnologyBadge code="linux" />
- </article>
+ <PortfolioProject
+ title="Vigo 360"
+ summary="Blog sobre Vigo y su entorno, orientado principalmente a hablar de movilidad y toponimia. Desarrollado en Go, con base de datos MySQL y desplegado sobre VPS administrado por mí mismo."
+ tags={["go", "mysql", "ubuntu"]}
+ githubLink="https://github.com/arielcostas/vigo360"
+ detailsLink="/portfolio/vigo-360"
+ onlineLink="https://vigo360.es"
+ />
</section>
</Layout>
<style>
section {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
- gap: 1rem;
- }
-
- article {
- padding: 1rem;
- border: 1px solid var(--accent);
- border-radius: 0.5rem;
- }
-
- article h3 {
- margin-top: 0;
+ grid-template-columns: repeat(auto-fill,minmax(350px,1fr));
+ gap: 1.5rem;
}
</style>
diff --git a/src/pages/blog/[id].astro b/src/pages/blog/[id].astro
index 935a796..3dc324a 100644
--- a/src/pages/blog/[id].astro
+++ b/src/pages/blog/[id].astro
@@ -1,5 +1,5 @@
---
-import Layout from "../../layouts/Layout.astro";
+import Layout from "@/layouts/Layout.astro";
import { getCollection, render } from "astro:content";
import { type GetStaticPaths } from "astro";
@@ -32,6 +32,7 @@ const schema = {
"@type": "BlogPosting",
headline: entry.data.title,
datePublished: entry.data.publishedAt.toISOString(),
+ keywords: entry.data.tags || [],
author: {
"@type": "Person",
name: "Ariel Costas Guerrero",
@@ -39,7 +40,8 @@ const schema = {
publisher: {
"@type": "Person",
name: "Ariel Costas Guerrero",
- logo: {
+ url: "https://www.costas.dev",
+ image: {
"@type": "ImageObject",
url: "https://www.costas.dev/favicon.png",
},
@@ -74,6 +76,10 @@ const schema = {
</small>
<Content />
+
+ <p>
+ <a href="/blog">Volver al blog</a>
+ </p>
</Layout>
<style lang="scss">
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
index b74781b..10a464b 100644
--- a/src/pages/blog/index.astro
+++ b/src/pages/blog/index.astro
@@ -1,318 +1,5 @@
---
-import { getCollection } from "astro:content";
-import Layout from "../../layouts/Layout.astro";
-
-const blogCollection = (await getCollection("blog")).sort((a, b) => {
- return b.data.publishedAt.getTime() - a.data.publishedAt.getTime();
-});
-
-// Agrupar artículos por fecha
-const groupedPosts = blogCollection.reduce(
- (acc: Record<string, any[]>, post) => {
- const year = post.data.publishedAt.getFullYear();
- const month = post.data.publishedAt.getMonth() + 1;
- const key = `${year}-${month}`;
- if (!acc[key]) {
- acc[key] = [];
- }
- acc[key].push(post);
- return acc;
- },
- {},
-);
-
-// Colección de todas las etiquetas únicas
-const allTags = [...new Set(blogCollection.flatMap(post => post.data.tags || []))].sort();
-
-function humaniseDate(date: Date) {
- const result = date.toLocaleDateString("es-ES", {
- month: "long",
- year: "numeric",
- });
- return result.charAt(0).toUpperCase() + result.slice(1);
-}
-
-const schema = {
- "@context": "https://schema.org",
- "@type": "Blog",
- headline: "Blog de Ariel Costas",
- description:
- "En este blog encontrarás artículos sobre desarrollo, tecnología y otras temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones son mías, y no representan a ninguna empresa o institución.",
- publisher: {
- "@type": "Person",
- name: "Ariel Costas",
- },
- author: {
- "@type": "Person",
- name: "Ariel Costas",
- },
-};
+import BlogListLayout from "@/layouts/BlogListLayout.astro";
---
-<Layout
- title="Blog"
- description="Artículos sobre desarrollo, tecnología y otras temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones son mías, y no representan a ninguna empresa o institución."
->
- <script
- is:inline
- type="application/ld+json"
- slot="head-jsonld"
- set:html={JSON.stringify(schema)}
- />
-
- <h1>Blog de Ariel Costas</h1>
-
- <p>
- En este blog encontrarás artículos sobre desarrollo, tecnología y otras
- temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones
- son mías, y no representan a ninguna empresa o institución.
- </p>
-
- {allTags.length > 0 && (
- <div class="tags-container">
- <h2>Etiquetas</h2>
- <div class="tag-filter">
- <button class="tag-button active" data-tag="all">Todas</button>
- {allTags.map((tag) => (
- <button class="tag-button" data-tag={tag}>{tag}</button>
- ))}
- </div>
- </div>
- )}
-
- <div id="blog-posts">
- {
- Object.entries(groupedPosts).map(([key, posts]) => (
- <section class="post-section" data-date={key}>
- <h2>{humaniseDate(new Date(key))}</h2>
- <ul>
- {posts.map((post) => {
- const postTags = post.data.tags || [];
- const tagsAttribute = postTags.join(',');
- return (
- <li class="post-item" data-tags={tagsAttribute}>
- <a href={`/blog/${post.id}`}>{post.data.title}</a>
- {postTags.length > 0 && (
- <ul class="post-tags">
- {postTags.map((tag: string) => (
- <li>
- <button class="tag-link" data-tag={tag}>
- {tag}
- </button>
- </li>
- ))}
- </ul>
- )}
- </li>
- );
- })}
- </ul>
- </section>
- ))
- }
- </div>
-
- <script>
- // Script para el filtrado de artículos por etiqueta
- document.addEventListener('DOMContentLoaded', () => {
- const tagButtons = document.querySelectorAll('.tag-button');
- const tagLinks = document.querySelectorAll('.tag-link');
- const postItems = document.querySelectorAll('.post-item');
- const postSections = document.querySelectorAll('.post-section');
-
- // Función para filtrar los artículos por etiqueta
- function filterByTag(tag: string) {
- // Primero, restablecer la visibilidad de todos los elementos
- postItems.forEach(item => {
- (item as HTMLElement).style.display = '';
- });
- postSections.forEach(section => {
- (section as HTMLElement).style.display = '';
- });
-
- // Si no es 'todas', filtrar por etiqueta
- if (tag !== 'all') {
- postItems.forEach(item => {
- const itemEl = item as HTMLElement;
- const tagsAttr = itemEl.dataset.tags || '';
- const itemTags = tagsAttr ? tagsAttr.split(',') : [];
- if (!itemTags.includes(tag)) {
- itemEl.style.display = 'none';
- }
- });
-
- // Ocultar secciones que no tienen artículos visibles
- postSections.forEach(section => {
- const items = section.querySelectorAll('.post-item');
- let allHidden = true;
-
- items.forEach(item => {
- if ((item as HTMLElement).style.display !== 'none') {
- allHidden = false;
- }
- });
-
- if (allHidden) {
- (section as HTMLElement).style.display = 'none';
- }
- });
- }
-
- // Actualizar estado activo de los botones
- tagButtons.forEach(button => {
- if ((button as HTMLElement).dataset.tag === tag) {
- button.classList.add('active');
- } else {
- button.classList.remove('active');
- }
- });
-
- // Actualizar URL con el parámetro de consulta
- if (tag === 'all') {
- history.replaceState(null, document.title, window.location.pathname);
- } else {
- history.replaceState(null, document.title, `?tag=${encodeURIComponent(tag)}`);
- }
-
- // Añadir un log para depuración
- console.log(`Filtrado por etiqueta: ${tag}`);
- console.log(`Artículos visibles: ${document.querySelectorAll('.post-item:not([style*="display: none"])')?.length || 0}`);
- }
-
- // Eventos de clic para los botones de etiquetas
- tagButtons.forEach(button => {
- button.addEventListener('click', () => {
- const tag = (button as HTMLElement).dataset.tag;
- if (tag) filterByTag(tag);
- });
- });
-
- // Eventos de clic para los enlaces de etiquetas dentro de los posts
- tagLinks.forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
- const tag = (link as HTMLElement).dataset.tag;
- if (tag) filterByTag(tag);
-
- // Desplazamiento suave hacia arriba para ver todos los resultados
- const tagsContainer = document.querySelector('.tags-container');
- if (tagsContainer) {
- window.scrollTo({
- top: (tagsContainer as HTMLElement).offsetTop - 20,
- behavior: 'smooth'
- });
- }
- });
- });
-
- // Verificar si hay un parámetro de consulta para filtrar
- const urlParams = new URLSearchParams(window.location.search);
- const tagParam = urlParams.get('tag');
-
- if (tagParam) {
- const tagExists = Array.from(tagButtons).some(button =>
- (button as HTMLElement).dataset.tag === tagParam
- );
- if (tagExists) {
- filterByTag(tagParam);
- }
- }
- });
- </script>
-</Layout>
-
-<style lang="scss">
- @use "../../../styles/variables" as v;
- @use "sass:color";
-
- .tags-container {
- margin-bottom: 2rem;
- }
-
- .tag-filter {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-bottom: 1.5rem;
- }
-
- .tag-button {
- padding: 0.25rem 0.6rem;
- background-color: v.$light;
- color: v.$accent;
- border: 1px solid v.$accent;
- border-radius: 1.5rem;
- font-size: 0.85rem;
- font-family: v.$monoFontStack;
- cursor: pointer;
- transition: all 0.2s ease;
- box-shadow: v.$shadow;
-
- &:hover {
- background-color: color.adjust(v.$accent, $lightness: 45%);
- color: v.$accentDark;
- transform: translateY(-1px);
- }
-
- &.active {
- background-color: v.$accent;
- color: v.$lightAlt;
- border-color: v.$accentDark;
- }
- }
-
- .post-tags {
- display: inline-flex;
- list-style: none;
- margin: 0;
- padding: 0;
- gap: 0.25rem;
- margin-left: 0.5rem;
- }
-
- .post-tags li {
- display: inline;
- }
-
- .tag-link {
- display: inline-block;
- padding: 0.1rem 0.4rem;
- background-color: color.adjust(v.$background, $lightness: -3%);
- color: v.$accentDark;
- border: none;
- border-radius: 1rem;
- font-size: 0.75rem;
- font-family: v.$monoFontStack;
- text-decoration: none;
- cursor: pointer;
- transition: all 0.2s ease;
-
- &:hover {
- background-color: color.adjust(v.$accent, $lightness: 45%);
- color: v.$accentDark;
- transform: translateY(-1px);
- }
- }
-
- /* Efecto de transición para el filtrado */
- .post-item {
- transition: all 0.3s ease;
- }
-
- .post-section {
- transition: opacity 0.3s ease;
- }
-
- #blog-posts {
- min-height: 200px;
- }
-
- /* Mejora el aspecto de los enlaces de posts */
- .post-item {
- margin-bottom: 0.5rem;
-
- a {
- font-weight: 500;
- }
- }
-</style>
+<BlogListLayout />
diff --git a/src/pages/portfolio/[id].astro b/src/pages/portfolio/[id].astro
index b92ecbd..601ff84 100644
--- a/src/pages/portfolio/[id].astro
+++ b/src/pages/portfolio/[id].astro
@@ -1,12 +1,7 @@
---
-import Layout from "../../layouts/Layout.astro";
-import { getCollection, render } from "astro:content";
-import { type GetStaticPaths } from "astro";
-import TechnologyBadge from "../../partials/TechnologyBadge.astro";
-
-interface Props {
- entry: any;
-}
+import type { GetStaticPaths } from "astro";
+import { getCollection } from "astro:content";
+import PortfolioItemLayout from "@/layouts/PortfolioItemLayout.astro";
export const getStaticPaths: GetStaticPaths = async () => {
const entries = await getCollection("portfolio");
@@ -17,50 +12,6 @@ export const getStaticPaths: GetStaticPaths = async () => {
};
const { entry } = Astro.props;
-const { Content } = await render(entry);
---
-<Layout title={entry.data.title} description={entry.data.description}>
- <a id="link-back" href="/portfolio">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- class="w-6 h-6 inline-block mr-2"
- >
- <polyline points="15 18 9 12 15 6"></polyline>
- </svg>
- Volver al portfolio
- </a>
-
- <h1>{entry.data.title}</h1>
-
- <Content />
-
- <h2>Tecnologías utilizadas</h2>
-
- {
- entry.data.technologies.map((technology: string) => (
- <TechnologyBadge size="small" code={technology} />
- ))
- }
-</Layout>
-
-<style>
- a#link-back {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- text-decoration: none;
- text-transform: uppercase;
- transition: color 0.2s ease-in-out;
- }
-
- a#link-back svg {
- height: 1em;
- }
-</style>
+<PortfolioItemLayout entry={entry} />
diff --git a/src/pages/portfolio/index.astro b/src/pages/portfolio/index.astro
index 17c4637..f5f1250 100644
--- a/src/pages/portfolio/index.astro
+++ b/src/pages/portfolio/index.astro
@@ -1,5 +1,5 @@
---
-import PortfolioPageLayout from "../../layouts/PortfolioPageLayout.astro";
+import PortfolioPageLayout from "@/layouts/PortfolioPageLayout.astro";
---
<PortfolioPageLayout />
diff --git a/src/partials/Footer.astro b/src/partials/Footer.astro
index bc30217..213aaa8 100644
--- a/src/partials/Footer.astro
+++ b/src/partials/Footer.astro
@@ -39,7 +39,7 @@ import t from "../i18n/es.json";
}
p {
- max-width: 82ch;
+ max-width: 60ch;
margin-inline: auto;
}
}
diff --git a/src/partials/Header.astro b/src/partials/Header.astro
index 2ca99bb..500fadd 100644
--- a/src/partials/Header.astro
+++ b/src/partials/Header.astro
@@ -48,11 +48,15 @@ import t from "../i18n/es.json";
}
@media (max-width: $breakpointTablet) {
- }
+ header {
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
- @media (min-width: $breakpointTablet) {
+ a {
+ font-size: 1.5rem;
+ }
+ }
}
- @media (min-width: $breakpointDesktop) {
- }
</style>