Generate a PDF from Dynamic Data in Meteor.js Tutorial

I just wrote up a little PDF tutorial for those who need help with generating PDF’s in their Meteor apps. Here is the link http://ryanswapp.com/2015/11/27/generate-a-pdf-from-dynamic-data-in-meteor-js/

Let me know if you have any questions or suggestions!

9 Likes

To your knowledge, is there a way to generate PDFs that can be downloaded using Safari? Either server side / client side, but using Flow Router. I’ve tried every package on atmopshere but none of them works as expected - Safari can’t download blobs/data64 files. I think I saw a snippet around here that uses Iron Router but haven’t got around to testing it yet.
I like this approach because it gives you a great deal of control over the layout, but it has the same problem with Safari.

Ah, yes, you will run into trouble with the window.open method when using Safari. I’d suggest uploading your generated PDF’s to a service like AWS S3 and then using the link that S3 gives you to trigger a download. For instance, this is how I do it in one of my apps:

webshot(html_string, fileName, options, function(err) {
                fs.readFile(fileName, function (err, data) {
                    if (err) {
                        return console.log(err);
                    }

                    var reportPrefix = prefix + "/reports/";
                    var reportKey = reportPrefix + cleanName + '-report-' + randomString();
                    var reportLink = 'https://s3-us-west-2.amazonaws.com/<bucket>/' + reportKey; // replace bucket with your s3 bucket

                    var params = {
                        Key: reportKey,
                        Body: data,
                        Bucket: Meteor.settings.awsBucket,
                        ContentType: 'application/pdf'
                    };

                    s3.putObject(params, function(err, data){
                        if (err) {
                            fs.unlinkSync(fileName);

                            console.log(err);
                        } else {
                            fs.unlinkSync(fileName);

                            fut.return({
                                key: reportKey,
                                link: reportLink 
                            });

                        }
                    });
                });
            });

Note: I’m only showing a tiny portion of the method so a few variable names are declared elsewhere.

I then can use the reportLink to trigger a download of the PDF from anywhere in my app.

<a href="<amazon-link>">Download PDF</a>

This method works really well for me.

1 Like

I’ve found another way you can accomplish this without uploading the file to S3 / without using Iron Router.
You’d have to use a server-side router, like meteorhacks:picker and set up a route where you generate the pdf and return it as a response.

// server side
Picker.route('/generate/pdf/:id', function(params, req, res, next) {
    var Future, Webshot, fileName, fs, fut, html, options;
    var vendor; 
    fs = Meteor.npmRequire('fs');
    Future = Npm.require('fibers/future');
    Webshot = Meteor.npmRequire('webshot');
    fut = new Future();
    SSR.compileTemplate('template', Assets.getText('pdfTemplate.html')); // must be in the 'private' folder
    vendor = Vendors.findOne({_id: params.id});
    if (vendor) {
        fileName = "Invoice - " + vendor.name + ".pdf";
        html = SSR.render('template', {
            vendor: vendor
        });
    }
    options = {
        phantomConfig: {
            'ignore-ssl-errors': 'true'
        },
        siteType: 'html',
        paperSize: {
            format: 'A4',
            orientation: 'portrait',
            margin: '1cm'
        }
    };
    Webshot(html, fileName, options, function(err) {
        fs.readFile(fileName, function(err, data) {
            if (err) {
                return console.log(err);
            }
            fs.unlinkSync(fileName);
            fut.return(data);
        });
    });
    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename='+fileName
    });
    res.end(fut.wait());
});

On the client side, it’s just as easy as calling the route like this:

<a href="/generate/pdf/{{ _id }}" download="">

This works in every browser, and can be used along with FlowRouter.

Ya I had previously used a solution like this and it works pretty good as long as the pdf you’re generating doesn’t contain any sensitive data. Anyone who has access to a document _id can send it to that route and they’ve got your data. It is quite a bit more complex to authenticate a user when using a server side route and I’ve found it overall more secure to just use a method (the route option requires you to use custom cookie or JWT based authentication). You definitely don’t need to send your PDF to S3 in order to prepare it for download. One option would be to use a tool like JSZip to generate a zip file containing your PDF and then using a JS download plugin to download it.

On the server side you can use the Meteor.userId() to check weather or not you generate the PDF document and return it. That should allow you to protect your sensitive data.

Meteor.userId() doesn’t work in an Iron Router server route unfortunately :frowning:

Then switching to Flow Router would be your best shot. But I’m sure you have already considered that :slight_smile:

Related topic discussed : https://medium.com/@satyavh/using-flow-router-for-authentication-ba7bb2644f42

Good solution. Is server side router a part of FlowRouter and put in server folder?

Yup :smile: I use FlowRouter for Blaze and React work with Meteor (I was just mentioning that you don’t have access to Meteor.userId() in a server side route just in case someone ran into that issue). With regard to PDF generation, I’ve found it far more effective to just implement a Meteor method. That way you can use Meteor.userId() and all the other Meteor goodies :thumbsup:

No, it’s actually an entirely different package, just for server-side routing: meteorhacks:picker. You’ll have to define your routes a bit differently than you would normally do in FlowRouter. Also if security is a concern, I’m thinking maybe you can use a server-side route to call a method and return its result, as per @ryanswapp’s suggestion (haven’t tried this, so I can’t say for sure if it works or not).

This is a good article that explains a bit the inner-workings of Picker and how to use it.

I got 404 when clicking the link

1 Like

Looks like the tutorial moved to Medium:

1 Like