Índice de contenido
- Vista general de los libros y secciones
- Proceso de renderizado previo
- Diferencias entre PDF y EPUB
- Generar el EPUB desde PHP
- Visualización del resultado
- Pre Renderizado de contenido HTML, clave para generar todo tipo de contenido
- ¿Qué es exactamente y cómo funciona?
- Implementación del pre render
- Ventajas del enfoque manual
- Estructura del sistema
- ¿Qué puedo hacer con el resultado?
Voy a explicarte, de manera resumida, cómo puedes generar un archivo EPUB (libro en formato EPUB) con tu proyecto en Laravel. Realmente, estos mismos pasos los puedes aplicar a cualquier proyecto en PHP, incluso si es código nativo. Así que todo este código puedes adaptarlo directamente a PHP, ya que, en un 90%, Laravel no tiene nada que ver aquí. No estamos usando nada propio de Laravel como tal.
La parte que sí se relaciona con Laravel es simplemente para devolver los controladores, modelos y poco más. Toda la integración restante la puedes adaptar perfectamente a cualquier otro entorno.
Vista general de los libros y secciones
Voy a mostrarte cómo funciona. Aquí tengo mis libros —como te decía, es un código que ya tengo trabajado. Cada libro tiene secciones, que puedes ver por acá:
class BookSection extends Model
{
use HasFactory;
protected $fillable = ['title', 'description', 'orden', 'content', 'posted', 'book_id', 'content_render'];
public function book()
{
return $this->belongsTo(Book::class);
}
} Proceso de renderizado previo
Este proceso me permite hacer lo que yo llamo un prerenderizado. Por ejemplo, para utilizar resaltado de código (highlight.js), necesito que la página se ejecute primero en el cliente para visualizar correctamente el contenido, y ese es el contenido que guardo en cada sección.
No modifico el contenido original. Guardo este contenido ya procesado en un campo oculto que llamo content_render, donde almaceno el HTML completo dividido por bloques. Al tenerlo dividido así, puedo iterarlo fácilmente. Cada bloque corresponde a un capítulo, lo que me permite procesarlo de forma independiente.
Esto es crucial porque necesito que el código esté bien formateado, especialmente en etiquetas <pre>.
Diferencias entre PDF y EPUB
Aquí surge una diferencia importante:
- Para PDF, necesito un único documento HTML, que se convierte en PDF directamente, antes, vimos como generar un PDF con Laravel y DoomPDF.
- Para EPUB, quiero dividir el contenido en capítulos, lo cual es lo tradicional, y además el paquete que uso lo permite. Cada capítulo es un documento independiente.
Generar el EPUB desde PHP
No voy a entrar mucho en detalle aquí, porque esta parte es algo más “a mi manera”. Lo explicaré mejor en el curso. Pero en resumen, lo que hago es componer manualmente cada capítulo. Por ejemplo, tengo una sección de introducción que incluye una imagen (que agrego desde PHP) y un título.
Voy tomando la etiqueta <head> de alguna de las secciones y, con eso, armo manualmente el HTML de cada capítulo, que luego agrego a la estructura final del EPUB.
Las imágenes necesitan tener ruta relativa en EPUB, a diferencia del PDF que usa rutas absolutas. También le doy formato a las imágenes, y otras cositas que voy ajustando según detecto necesidades.
Para cada sección, hago una petición con Axios para obtener su contenido renderizado, y cada sección es en sí misma una página HTML independiente. Hay muchas formas de hacerlo, pero yo armé un híbrido para que funcione tanto para PDF como para EPUB.
Lo primero que hago es crear una instancia del objeto EPUB y empiezo a procesar las imágenes de la carátula. Puedes cambiar la extensión .epub a .zip y verás que el EPUB es básicamente un archivo comprimido con carpetas, como una para imágenes.
Por eso, tengo que mover todas las imágenes del proyecto Laravel hacia esa estructura de EPUB que estamos generando. Las rutas se van guardando en la instancia del objeto EPUB.
También agrego metadatos como el título, el autor, etc. Para la imagen de la carátula, guardo tanto en formato WebP como en PNG, ya que algunos visores como el de Apple no soportan WebP y generan errores.
Este tipo de detalles son importantes, sobre todo si planeas distribuir tu EPUB en tiendas como Google Play o Apple Books, que tienen validadores estrictos.
Añado estilos CSS personalizados para títulos, párrafos, código, etc. Todo esto es completamente configurable. Luego genero el nombre del archivo .epub, utilizando el nombre del libro y eliminando caracteres especiales.
Agrego las imágenes al paquete, proceso la introducción manualmente si no es un capítulo separado, y genero el contenido HTML de cada capítulo de manera estructurada.
Cada sección o capítulo debe ser un documento XHTML válido. No sirve un HTML común, porque los lectores EPUB requieren un formato más estricto.
Aquí ajusto la cabecera del documento y añado la declaración correcta de DOCTYPE y de xmlns, porque si no, el visor de Apple, por ejemplo, no lo abrirá correctamente.
Una vez que todos los capítulos están definidos y agregados, genero el archivo EPUB con el método saveBook().
En mi caso, lo almaceno en una carpeta específica del proyecto. Desde la interfaz de mi aplicación, puedo presionar un botón para ejecutar todo este proceso de forma automática. Eso es lo que hace este botón de "Generar".
function generateEpub()
{
$pathImages = "/images/example/libros/" . $this->book->path . "/";
$images = FacadesFile::files(public_path($pathImages));
$filePath = storage_path('book/');
// Crear una nueva instancia de EPub
$epub = new EPub();
//*** */ Configurar los metadatos del libro
$epub->setTitle($this->book->title);
$epub->setIdentifier(route('web-book-show', ['post_url_clean' => $this->book->post->url_clean]), EPub::IDENTIFIER_URI);
$epub->setLanguage($this->book->post->language == 'spanish' ? 'es' : 'en');
$epub->setDescription($this->book->description);
$epub->setAuthor('Andrés Cruz Yoris', 'Autor, Andrés Cruz Yoris');
$epub->setPublisher('DesarrolloLibre', route('web-book-show', ['post_url_clean' => $this->book->post->url_clean]));
$epub->setRights('Derechos reservados © ' . date('Y') . ' DesarrolloLibre');
$epub->setDate(time()); // Fecha actual
//*** Agregar capítulos al libro
foreach ($this->book->sections()->where('posted', 'yes')->where('orden', '>', 0)->orderBy('orden')->get() as $key => $s) {
// Acomoda las rutas de las imagenes para que sean relativas
$html = str_replace("/public/images/example/libros/", "/images/example/libros/", $s->content_render); // quita el public
$html = str_replace("/images/example/libros/", "images/example/libros/", $html); // quita un / para que la ruta sea relativa
// agrega el capitulo al libro
$epub->addChapter($s->title, "chapter" . $key + 1 . ".xhtml", '<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' . $html, true);
}
// Finalizar la creación del ePub
$epub->finalize();
// Guardar el archivo ePub en el almacenamiento de Laravel
$epub->saveBook($fileName, $filePath);
return response()->download($filePath . $fileName);
}Este es el paquete:
https://github.com/Grandt/PHPePub
Visualización del resultado
Al abrir el archivo generado en el visor de Apple, se muestran correctamente la carátula y el índice. También tengo navegación por capítulos y, dependiendo del visor, puede que se vea o no el resaltado de código.
Por ejemplo, Calibre sí muestra el código con estilos, pero el visor de Apple es más limitado en este aspecto. Todo depende de cuánto del CSS interprete el visor.
Pre Renderizado de contenido HTML, clave para generar todo tipo de contenido
Quiero hablarte de algo que considero muy interesante, que además creo que fue una muy buena idea que se me ocurrió, y que me ha servido muchísimo. Realmente, para dos cosas. A esto lo llamo "pre render de contenido", un nombre quizá algo grandilocuente, pero te voy a explicar brevemente a qué me refiero con esto.
Es un concepto del que ya te he hablado antes. Recuerda que en otros videos te he mostrado cómo generar un PDF y los mismos Epubs que te comenté en la parte de arriba y cómo optimizar un blog. Incluso publiqué una lista de reproducción en la que te muestro cómo llevé el puntaje del blog de 33 a 100 en Google Speed Test —o más o menos por ahí, ya sabes que ese test es un poco mañoso—. Para mí, la clave de esa mejora fue precisamente esto: el pre render de contenido.
¿Qué es exactamente y cómo funciona?
Todo comienza con mi CKEditor de contenido, donde tengo almacenado el contenido base. Puede ser una publicación o, como en este caso, un fragmento de un libro, es decir, un capítulo.
Ya tengo algunas cosas marcadas: código, párrafos, encabezados (H1, H2, etc.). Pero, por más que tengamos estructura, muchas veces —sobre todo en el caso de un blog— hacemos cambios adicionales. Por ejemplo, en los bloques de código, lo usual es aplicar un plugin de resaltado de sintaxis.
Sin embargo, lo que ves en este fragmento es solo una etiqueta <pre> con <code>. No hay ningún resaltado visual. Si recuerdas el video sobre generación de PDF, te mostraba cómo con Highlight.js se añaden etiquetas <span>, clases y colores.
Highlight.js solo se ejecuta del lado del cliente, es decir, cuando el usuario accede al navegador. Para mí eso era un problema:
- En el blog, quería quitar ese plugin de Highlight.js para mejorar el SEO (aunque no era muy pesado).
- En los libros, quería que el resultado ya viniera resaltado, sin depender del navegador.
Si has leído alguno de mis libros, ya sabrás que el código aparece en bloques grises sin resaltado, porque no había procesado nada. Justamente eso es lo que quería cambiar.
Implementación del pre render
Ya te lo había mostrado un poco: tengo un botón o enlace que me permite generar el contenido pre renderizado y de allí, mediante JS guardo los bloques HTML definidos con IDs que me interese guardar, también, doy un procesamiento previo para quitar, agregar atributos u otra estructura que quiero que tenga, reformateo para cumplir estándares como XHTML 5..
Existen paquetes en Node que permiten hacer eso: ejecutan el JavaScript, extraen el HTML y lo guardan. Yo no usé ninguno; preferí hacerlo de forma manual dentro de una vista editable, lo que me da mayor flexibilidad. Así, si luego quiero cambiar algo manualmente, puedo hacerlo.
Ventajas del enfoque manual
Me permite:
- Revisar y corregir contenido visualmente.
- Activar o desactivar cosas como índices o resaltado.
- Inyectar clases personalizadas en el HTML generado.
- Todo esto lo hago en una página que contiene HTML, clases para estilo y bastante JavaScript. Desde ahí puedo controlar lo que sucede antes de guardar el resultado final.
Estructura del sistema
Siempre trabajamos con dos campos:
- content: es el contenido editable, lo que escribes o importas.
- content_render: es el contenido renderizado, el resultado final que se guarda después del proceso de pre render.
Esto me permite mantener ambos: el original editable y el optimizado para mostrar o generar el PDF.
¿Qué puedo hacer con el resultado?
¡Muchas cosas! Dos ejemplos claros:
- Mostrar un post del blog ya procesado, sin necesidad de resaltar en el cliente.
- Generar libros con bloques de código ya optimizados.
Este enfoque me encanta porque me da control total sobre el contenido final, sin depender de librerías externas y en tiempo real.