Content Index
- Overview of books and sections
- Pre-rendering process
- Differences between PDF and EPUB
- Generate the EPUB from PHP
- Viewing the result
- Pre-rendering HTML content, key to generating all types of content
- What exactly is it and how does it work?
- Pre-rendering implementation
- Advantages of the manual approach
- System Structure
- What can I do with the result?
I'm going to briefly explain how you can generate an EPUB file (a book in EPUB format) with your Laravel project. You can actually apply these same steps to any PHP project, even if it's native code. So, you can adapt all this code directly to PHP, since Laravel has nothing to do with it. We're not using anything specific to Laravel itself.
The part that does relate to Laravel is simply to return the controllers, models, and little else. You can perfectly adapt all the remaining integration to any other environment.
Overview of books and sections
I'll show you how it works. Here are my books—as I said, it's a code I've already worked on. Each book has sections, which you can see here:
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);
}
} Pre-rendering process
This process allows me to do what I call pre-rendering. For example, to use code highlighting (highlight.js), I need the page to run first on the client to properly display the content, and that's the content I save in each section.
I don't modify the original content. I save this already rendered content in a hidden field I call content_render, where I store the full HTML divided into blocks. By dividing it this way, I can easily iterate through it. Each block corresponds to a chapter, which allows me to process it independently.
This is crucial because I need the code to be well-formatted, especially in the <pre> tags.
Differences between PDF and EPUB
An important difference arises here:
- For PDF, I need a single HTML document, which is converted directly to PDF. We previously saw how to generate a PDF with Laravel and DoomPDF.
- For EPUB, I want to divide the content into chapters, which is the traditional approach, and the package I use allows it. Each chapter is a separate document.
Generate the EPUB from PHP
I won't go into much detail here, because this part is more "my way." I'll explain it better in the course. But in short, what I do is manually compose each chapter. For example, I have an introduction section that includes an image (which I add from PHP) and a title.
I take the <head> tag from one of the sections and, with that, manually put together the HTML for each chapter, which I then add to the final EPUB structure.
Images need to have relative paths in EPUB, unlike PDF, which uses absolute paths. I also format the images and adjust other things as needed.
For each section, I make a request with Axios to get its rendered content, and each section is a separate HTML page in itself. There are many ways to do this, but I built a hybrid so it works for both PDF and EPUB.
The first thing I do is create an instance of the EPUB object and start processing the cover images. You can change the .epub extension to .zip, and you'll see that the EPUB is basically a compressed file with folders, like one for images.
Therefore, I need to move all the images from the Laravel project into the EPUB structure we're generating. The paths are saved in the EPUB object instance.
I also add metadata such as the title, author, etc. For the cover image, I save in both WebP and PNG formats, as some viewers, like Apple's, don't support WebP and generate errors.
These types of details are important, especially if you plan to distribute your EPUB on stores like Google Play or Apple Books, which have strict validators.
I add custom CSS styles for headings, paragraphs, code, etc. All of this is completely configurable. Then I generate the .epub filename, using the book name and removing special characters.
I add the images to the package, process the introduction manually if it's not a separate chapter, and generate the structured HTML content for each chapter.
Each section or chapter must be a valid XHTML document. Plain HTML isn't suitable because EPUB readers require stricter formatting.
Here, I adjust the document header and add the correct DOCTYPE and xmlns declaration, because otherwise, the Apple viewer, for example, won't open it correctly.
Once all the chapters are defined and added, I generate the EPUB file using the saveBook() method.
In my case, I store it in a specific folder in the project. From my application interface, I can press a button to run this entire process automatically. That's what this "Generate" button does.
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);
}This is the package:
https://github.com/Grandt/PHPePub
Viewing the result
When opening the generated file in Apple's viewer, the cover art and table of contents are displayed correctly. I also have chapter navigation, and depending on the viewer, code highlighting may or may not be visible.
For example, Calibre does display styled code, but Apple's viewer is more limited in this regard. It all depends on how much of the CSS the viewer interprets.
Pre-rendering HTML content, key to generating all types of content
I want to tell you about something I find very interesting, which I also think was a very good idea I came up with, and which has been very useful to me. Actually, for two reasons. I call this "content pre-rendering," a perhaps somewhat grandiose name, but I'll briefly explain what I mean by it.
It's a concept I've talked about before. Remember that in other videos, I've shown you how to generate a PDF and how to optimize a blog. I even published a playlist showing you how I took my blog's score from 33 to 100 on Google Speed ​​Test—or somewhere around there, as you know, that test is a bit tricky. For me, the key to that improvement was precisely this: content pre-rendering.
What exactly is it and how does it work?
It all starts with my Content Editor, where I store the base content. It can be a post or, as in this case, an excerpt from a book, i.e., a chapter.
I already have some things highlighted: code, paragraphs, headings (H1, H2, etc.). But even if we have structure, we often make additional changes—especially in the case of a blog—to code blocks. For example, it's common to apply a syntax highlighting plugin.
However, what you see in this snippet is just a <pre> tag with <code>. There's no visual highlighting. If you remember the video on PDF generation, I showed you how Highlight.js adds <span> tags, classes, and colors.
Highlight.js only runs on the client side, that is, when the user accesses the browser. For me, that was a problem:
- On the blog, I wanted to remove that Highlight.js plugin to improve SEO (although it wasn't very heavy).
- In the books, I wanted the output to be already highlighted, regardless of the browser.
If you've read any of my books, you'll already know that the code appears in gray blocks without highlighting because it hadn't processed anything. That's exactly what I wanted to change.
Pre-rendering implementation
I've already shown you a bit about it: I have a button or link that allows me to generate pre-rendered content, and from there, using JS, I save the HTML blocks defined with IDs that I want to save. I also perform pre-processing to remove, add attributes, or any other structure I want, and reformat to comply with standards like XHTML 5.
There are packages in Node that allow you to do this: they execute the JavaScript, extract the HTML, and save it. I didn't use any; I preferred to do it manually within an editable view, which gives me greater flexibility. That way, if I want to change something manually later, I can do so.
Advantages of the manual approach
It allows me to:
- Visually review and correct content.
- Enable or disable things like indexes or highlighting.
- Inject custom classes into the generated HTML.
- I do all of this on a page that contains HTML, styling classes, and a fair amount of JavaScript. From there, I can control what happens before saving the final result.
System Structure
We always work with two fields:
- content: This is the editable content, what you write or import.
- content_render: This is the rendered content, the final result saved after the pre-render process.
This allows me to keep both: the editable original and the optimized content for display or PDF generation.
What can I do with the result?
Many things! Two clear examples:
- Display an already processed blog post, without having to highlight it in the client.
- Generate books with already optimized code blocks.
I love this approach because it gives me full control over the final content, without depending on external libraries and in real time.