Blog / Portafolio — Arquitectura con AltoRouter
Este blog funciona como portafolio técnico y hub de documentación, construido en PHP
utilizando AltoRouter para el enrutamiento
y una estructura de carpetas por categorías dentro de /blog.
Cada post es un archivo PHP independiente con metadatos internos, y el sistema genera de forma dinámica:
rutas, listados, búsqueda, paginación y URLs limpias.
Objetivo del sistema
Diseñar un blog-portafolio que cumpla con:
- Baja fricción para crear nuevos posts: sólo se agrega un archivo PHP dentro de la categoría.
- URLs limpias y estables, controladas por una variable
$postSlug. - Separación clara por categorías (react, php, docker, laravel, vps-hosting, webs, etc.).
- SEO básico integrado: título, meta description y slug se leen directamente del archivo del post.
- Listados y búsqueda automáticos por categoría, sin escribir queries SQL.
Tecnologías y herramientas
- PHP 8+ — lógica de enrutamiento y lectura de archivos.
- AltoRouter — router minimalista basado en patrones de URL.
- Composer — gestión de dependencias.
- TailwindCSS via CDN — maquetado responsivo de listados y posts.
- Lucide Icons — iconografía ligera para la UI del blog.
- Arquitectura file-based — cada post es un archivo PHP dentro de su carpeta de categoría.
Estructura de carpetas
public_html/
└── web/
└── jair-rivera.qodexia.site/
├── blog/
│ ├── react/
│ │ ├── index.php
│ │ └── <post-react-*.php>
│ ├── php/
│ ├── docker/
│ ├── laravel/
│ ├── vps-hosting/
│ ├── webs/
│ │ ├── index.php
│ │ └── planeta-fiscal.php
│ │ └── real-estate-demo.php
│ ├── _init.php
│ ├── _sidebar.php
│ └── index.php // listado general del blog
├── routes/
│ ├── categories.php
│ ├── posts-react.php
│ ├── posts-php.php
│ ├── posts-docker.php
│ ├── posts-laravel.php
│ ├── posts-vps-hosting.php
│ └── posts-webs.php
├── home.php
├── index.php // Front controller con AltoRouter
├── composer.json
└── vendor/ // AltoRouter + autoload
Flujo 1 — Enrutamiento principal con AltoRouter
El archivo index.php en la raíz actúa como front controller:
// Configuración base
require __DIR__ . '/vendor/autoload.php';
$router = new AltoRouter();
$router->setBasePath('');
// Rutas base
$router->map('GET', '/', function () {
require __DIR__ . '/home.php';
});
$router->map('GET', '/blog', function () {
require __DIR__ . '/blog/index.php';
});
// Rutas externas (categorías + posts)
require __DIR__ . '/routes/categories.php';
require __DIR__ . '/routes/posts-react.php';
require __DIR__ . '/routes/posts-php.php';
require __DIR__ . '/routes/posts-docker.php';
require __DIR__ . '/routes/posts-laravel.php';
require __DIR__ . '/routes/posts-vps-hosting.php';
require __DIR__ . '/routes/posts-webs.php';
// Resolución de la ruta
$match = $router->match();
if ($match && is_callable($match['target'])) {
call_user_func_array($match['target'], $match['params']);
} else {
http_response_code(404);
echo "<p>404 — Página no encontrada</p>";
}
De esta forma, el router:
- Resuelve
/y/blogcon archivos concretos. - Delega las rutas de categorías y posts a archivos dentro de
/routes. - Centraliza la lógica 404 en un solo lugar.
Flujo 2 — Rutas dinámicas para posts por categoría
Cada archivo routes/posts-*.php se encarga de registrar las rutas de los posts de una categoría,
leyendo los archivos de la carpeta correspondiente:
// Ejemplo: routes/posts-webs.php
$categoryFolder = 'webs';
$categoryBase = '/blog/' . $categoryFolder;
$postsDir = __DIR__ . '/../blog/' . $categoryFolder;
$files = glob($postsDir . '/*.php') ?: [];
foreach ($files as $filePath) {
$file = basename($filePath);
if (in_array(strtolower($file), ['index.php'])) {
continue; // no es un post
}
$code = @file_get_contents($filePath);
if ($code === false) continue;
// 1) Obtener $postSlug desde el archivo del post
$slug = null;
if (preg_match('/\$postSlug\s*=\s*[\'"](.*?)[\'"]\s*;/i', $code, $m)) {
$slug = trim($m[1]);
}
// Fallback: usar nombre de archivo sin .php
if ($slug === null || $slug === '') {
$slug = basename($file, '.php');
}
$slug = trim($slug, "/ \t\n\r\0\x0B");
// 2) Registrar ruta dinámica en AltoRouter
$router->map('GET', $categoryBase . '/' . $slug, function () use ($categoryFolder, $file) {
require __DIR__ . '/../blog/' . $categoryFolder . '/' . $file;
});
}
Esto permite que:
- Cada nuevo post sólo requiera un archivo PHP con
$postSlug. - Las URLs sigan el patrón
/blog/<categoria>/<slug>sin configurar nada extra. - El slug sea independiente del nombre del archivo, lo que evita romper enlaces al renombrar.
Flujo 3 — Metadatos dentro de cada post
Cada post define su propio slug y fecha al inicio del archivo, antes del HTML:
<?php
$postSlug = 'planeta-fiscal-demo';
$postDate = '2025-11-18';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<title>Planeta Fiscal — Sitio demo contable</title>
<meta name="description"
content="Landing y sitio comercial para una firma contable con planes y membresías." />
...
</head>
Estos metadatos son reutilizados en:
- El sistema de rutas (para construir la URL del post).
- El listado por categoría (para mostrar título, descripción y fecha).
- El orden cronológico (usando
$postDateo elfilemtime()como backup).
Flujo 4 — Listado, búsqueda y paginación por categoría
Dentro de cada carpeta de categoría, el archivo index.php se encarga de:
- Detectar la categoría a partir del propio directorio (
$categoryFilter = basename(__DIR__)). - Leer todos los posts de la carpeta con
glob(). - Extraer:
- Título desde
<title>. - Descripción desde
<meta name="description">. - Slug desde la variable
$postSlugdel post. - Fecha normalizada (
$postDateofilemtime()).
- Título desde
- Ordenar el array de posts por fecha (más reciente → más antiguo).
- Aplicar filtro de búsqueda si llega
?q=texto. - Aplicar paginación si no hay búsqueda (10 posts por página).
A nivel conceptual el flujo es:
// 1. Cargar posts
$files = glob($postsDir . '/*.php') ?: [];
$posts = [];
// 2. Recorrer archivos, extraer metadatos, slug y fecha
// 3. Ordenar por timestamp descendente
usort($posts, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);
// 4. Si hay búsqueda (?q=), filtrar por título / descripción / slug
// 5. Si NO hay búsqueda, aplicar paginación (10 por página)
// 6. Pasar $postsPage al template para pintarlos
Flujo 5 — Interfaz del listado y sidebar reutilizable
El HTML de cada index.php de categoría:
- Muestra un header con breadcrumb: Inicio → Blog → Categoría.
- Renderiza tarjetas de posts con:
- Título, descripción corta y slug.
- Badge de categoría.
- Fecha formateada
Y-m-d.
- Incluye un componente de paginación con:
- Información: página actual, total de páginas y total de posts.
- Links Anterior / Siguiente.
- Números de página alrededor de la actual.
- Incluye un sidebar común vía
require __DIR__ . '/../_sidebar.php';con:- Buscador en la categoría (formulario con
q). - Link para volver al blog completo.
- Listado de categorías (React, PHP, Docker, VPS / Hosting, Laravel, Webs, etc.).
- Buscador en la categoría (formulario con
Resultados y aprendizajes
Este sistema de blog demuestra:
- Cómo usar AltoRouter para enrutamiento avanzado sin framework completo.
- Cómo construir un blog file-based donde el contenido vive en el sistema de archivos.
- Cómo extraer metadatos (título, descripción, slug, fecha) directamente de los archivos de contenido.
- Cómo implementar búsqueda ligera y paginación sólo con PHP nativo.
- Cómo mantener una UI consistente utilizando TailwindCSS y componentes reutilizables como el sidebar.