עיבוד PDF בצד השרת מתבניות HTML באמצעות PHP ו-Node.js

יצירת PDF ידנית גוזלת זמן ומאטה תהליכים עסקיים. אנחנו מפתחים ייצור PDF בצד השרת מ-HTML, ומבצעים אוטומציה של יצירת חשבוניות, אישורים ודוחות. הצוות שלנו מספק פרויקט סוהר—מבחירת הכלים ועד להטמעה ולתמיכה שוטפת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
עיבוד PDF בצד השרת מתבניות HTML באמצעות PHP ו-Node.js
בינוני
~3-5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

יצירת PDF אוטומטית מ-HTML

שימו לב: כאשר חשבונית מתעכבת בגלל יצירת PDF ידנית, העסק מפסיד כסף. בפרויקט אחד, יצירת 500 חשבוניות ביום לקחה 4 שעות של עבודה ידנית. אוטומטנו את התהליך: עכשיו קבצי PDF נוצרים 5 דקות לאחר ההזמנה, והלקוחות מקבלים אותם מיד. אשתף כיצד אנו מיישמים יצירת PDF בצד השרת באמצעות HTML-to-PDF. הלקוח שלנו—שירות הנהלת חשבונות מקוון—חסך 11–16 אלף דולר בשנה על ידי אוטומציה של יצירת אישורי ביצוע. יצירה ידנית עלתה לחברה 2.7–3.9 אלף דולר בחודש.

בעיות שאנו פותרים

פריסה מורכבת עם CSS. ספריות רבות לא מבינות Grid, Flexbox, משתנים. פתרון: Chrome ללא ממשק דרך Browsershot (PHP) או Puppeteer (Node.js). הם מרנדרים HTML כמו דפדפן—מושלם לחשבוניות, חוזים, דוחות.

גופנים וקירילית. ללא הגדרה נכונה, התווים הופכים לגיבוב. אנו כוללים Google Fonts דרך @import או מטמיעים גופנים מקומיים. ב-TCPDF אנו משתמשים ב-DejaVu Sans—הוא תומך בקירילית ללא בעיות.

ביצועים. יצירת PDF אחד דרך דפדפן לוקחת 2-5 שניות. ב-5000 מסמכים לשעה, זה קריטי. פתרון: תור אסינכרוני (Laravel Queue / Bull) ואחסון ב-S3. המשתמש מקבל תגובה מיידית, בעוד ה-PDF נוצר ברקע.

איך אנו עושים זאת: טכנולוגיות ומקרה בוחן

עבור לקוח—שירות הנהלת חשבונות מקוון—יישמנו יצירת אישורי ביצוע באמצעות Laravel 11. השתמשנו ב-spatie/browsershot (המבוסס על Puppeteer). התבנית נכתבה ב-Blade עם CSS Grid לטבלת השירותים.

Laravel: Browsershot (Puppeteer)

Browsershot משתמש ב-Chrome ללא ממשק כדי לרנדר HTML ל-PDF—תומך ב-CSS Grid, Flexbox, משתנים, גופנים.

use Spatie\Browsershot\Browsershot;

class InvoicePdfService
{
    public function generate(Invoice $invoice): string
    {
        $html = view('pdf.invoice', ['invoice' => $invoice])->render();
        $path = storage_path("app/invoices/invoice-{$invoice->id}.pdf");

        Browsershot::html($html)
            ->format('A4')
            ->margins(15, 15, 15, 15) // мм
            ->showBackground()
            ->emulateMedia('print')
            ->waitUntilNetworkIdle() // дождаться загрузки шрифтов
            ->save($path);

        return $path;
    }
}

// Controller
public function download(Invoice $invoice): Response
{
    $path = $this->invoicePdfService->generate($invoice);

    return response()->download(
        $path,
        "invoice-{$invoice->number}.pdf",
        ['Content-Type' => 'application/pdf']
    );
}
<!-- resources/views/pdf/invoice.blade.php -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: 'Inter', sans-serif;
      font-size: 12px;
      color: #1a1a1a;
    }
    .header {
      display: flex;
      justify-content: space-between;
      margin-bottom: 40px;
    }
    .invoice-number {
      font-size: 24px;
      font-weight: 700;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 20px;
    }
    th {
      background: #f3f4f6;
      padding: 8px;
      text-align: left;
      font-weight: 600;
    }
    td {
      padding: 8px;
      border-bottom: 1px solid #e5e7eb;
    }
    .total {
      font-size: 16px;
      font-weight: 700;
      text-align: right;
      margin-top: 20px;
    }
    @media print {
      .page-break {
        page-break-after: always;
      }
    }
  </style>
</head>
<body>
  <div class="header">
    <div>
      <img src="{{ public_path('logo.png') }}" height="40">
      <div>{{ $invoice->company->name }}</div>
    </div>
    <div>
      <div class="invoice-number">Счёт #{{ $invoice->number }}</div>
      <div>Дата: {{ $invoice->date->format('d.m.Y') }}</div>
    </div>
  </div>
  <table>
    <thead>
      <tr><th>Описание</th><th>Кол-во</th><th>Цена</th><th>Сумма</th></tr>
    </thead>
    <tbody>
      @foreach($invoice->items as $item)
      <tr>
        <td>{{ $item->description }}</td>
        <td>{{ $item->quantity }}</td>
        <td>{{ number_format($item->price, 2) }} $</td>
        <td>{{ number_format($item->total, 2) }} $</td>
      </tr>
      @endforeach
    </tbody>
  </table>
  <div class="total">Итого: {{ number_format($invoice->total, 2) }} $</div>
</body>
</html>

Node.js: Puppeteer

import puppeteer from 'puppeteer';
import Handlebars from 'handlebars';

async function generateInvoicePdf(invoice: Invoice): Promise<Buffer> {
  const templateSource = await fs.readFile('./templates/invoice.html', 'utf-8');
  const template = Handlebars.compile(templateSource);
  const html = template(invoice);

  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
  });

  try {
    const page = await browser.newPage();
    await page.setContent(html, { waitUntil: 'networkidle0' });
    return await page.pdf({
      format: 'A4',
      margin: {
        top: '15mm',
        right: '15mm',
        bottom: '15mm',
        left: '15mm'
      },
      printBackground: true,
    });
  } finally {
    await browser.close();
  }
}

TCPDF: יצירה מקורית ב-PHP (ללא דפדפן)

מתאים למסמכים פשוטים ללא CSS מורכב:

use TCPDF;

class ContractPdfService
{
    public function generate(Contract $contract): string
    {
        $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8');
        $pdf->SetCreator('MyApp');
        $pdf->SetAuthor($contract->company->name);
        $pdf->SetTitle('Договор №' . $contract->number);
        $pdf->SetFont('dejavusans', '', 10);
        $pdf->AddPage();

        $html = view('pdf.contract-simple', compact('contract'))->render();
        $pdf->writeHTML($html, true, false, true, false, '');

        $path = storage_path("app/contracts/contract-{$contract->id}.pdf");
        $pdf->Output($path, 'F');

        return $path;
    }
}

יצירה אסינכרונית בתור

class GenerateInvoicePdfJob implements ShouldQueue {
    public int $timeout = 120;

    public function __construct(private Invoice $invoice) {}

    public function handle(InvoicePdfService $service): void {
        $path = $service->generate($this->invoice);

        // Загрузить в S3
        $s3Key = "invoices/{$this->invoice->user_id}/{$this->invoice->id}.pdf";
        Storage::disk('s3')->put($s3Key, file_get_contents($path));

        $this->invoice->update(['pdf_key' => $s3Key, 'pdf_generated_at' => now()]);

        unlink($path);

        // Уведомить пользователя
        $this->invoice->user->notify(new InvoiceReadyNotification($this->invoice));
    }
}

איך לבחור בין Browsershot ל-TCPDF?

קריטריון Browsershot (Laravel) Puppeteer (Node.js) TCPDF (PHP)
CSS Grid/Flexbox נתמך נתמך לא נתמך
מהירות רינדור 2-5 שניות 2-5 שניות <1 שנייה
קירילית דרך Google Fonts דרך Google Fonts DejaVu מובנה
מורכבות התקנה בינונית בינונית נמוכה
שימוש בזיכרון ~200 MB ~200 MB ~50 MB

Browsershot מהיר פי 2 מ-TCPDF לפריסות מורכבות, אך לטבלאות פשוטות TCPDF חסכוני יותר בזיכרון.

תהליך

  1. ניתוח. זיהוי סוגי מסמכים (חשבוניות, חוזים, דוחות). הערכת מורכבות הפריסה.
  2. עיצוב. יצירת תבנית ב-Blade או Handlebars. הגדרת גופנים.
  3. יישום. כתיבת שירות יצירה, חיבור תור לעיבוד אסינכרוני.
  4. בדיקות. אימות על 50+ מסמכים. השוואת גודל ואיכות.
  5. השקה. הגדרת S3, CDN, ניטור (רישום שגיאות).

לוחות זמנים

  • יצירה בסיסית (Browsershot/Puppeteer לתבנית אחת): 2 עד 3 ימים.
  • עם תור אסינכרוני ו-S3: 3 עד 4 ימים.
  • עם חתימה דיגיטלית: הוספת יומיים.

מה כלול

  • פיתוח תבנית PDF המשלבת סגנון ארגוני.
  • הגדרת תור ואחסון בענן (S3, MinIO).
  • תיעוד API ליצירה.
  • העברת גישה לשרת ולמאגר הקוד.
  • הדרכת צוות על השקה וניטור.
  • תמיכה למשך שבועיים לאחר השחרור.

למה להפקיד בידינו את המשימה הזו?

יש לנו 10+ שנות ניסיון בפיתוח web ומעל 50 פרויקטים של יצירת PDF. אנו משתמשים רק בטכנולוגיות מוכחות: Laravel + Browsershot, Node.js + Puppeteer. אנו מבטיחים יציבות: הקוד נבדק, התור מנסה שוב במקרה של כשלים. קבלו ייעוץ לפרויקט שלכם—נעריך אותו תוך יום אחד.

רשימת בדיקה: שגיאות נפוצות ביצירת PDF

שגיאה סיבה פתרון
אלמנטים חורגים מהגבולות חסר @media print הוספת media query עם page-break
טקסט דבוק לקצוות לא הוגדרו שוליים הגדרת שוליים (15 מ"מ)
גיבוב במקום קירילית גופנים שגויים שימוש ב-DejaVu או Google Fonts
גודל PDF > 10 MB תמונות base64 גדולות אופטימיזציה, שימוש בקישורים חיצוניים
שגיאת רינדור תגי HTML לא סגורים אימות HTML לפני יצירה

הגדירו מגבלה על מספר העמודים וגודל הקובץ—זה ימנע תקיעות ביצירה. לפרטים נוספים, עיינו בתיעוד הרשמי של Puppeteer ב-pptr.dev. לפי ויקיפדיה, PDF הוא פורמט מסמכים נפוץ (מקור: ויקיפדיה).

הגדרת גופנים לקירילית יציבה

ב-TCPDF, השתמשו בגופן dejavusans—הוא מובנה ותומך בקירילית. ב-Browsershot, כללו Google Fonts דרך @import וודאו שהגופן נטען לפני היצירה. לסביבות לא מקוונות, הטמיעו את הגופן מקומית.