Desenvolvimento

24 jun, 2016

Escreva e implemente seu próprio clone hospedado em Readability

Publicidade

Readability.org é uma ferramenta útil, mas ela sofre por ser útil, popular e gratuita – o serviço muitas vezes está fora do ar ou lento.

Se você é um hard user do Readability, pode estar interessado em saber que existem muitas bibliotecas de software que tentam recriar a função principal do Readability – que tiram o corpo principal do texto de uma página carregada com anúncios e outras distrações. Algumas delas funcionam melhor do que outras, mas o Readability.js da Mozilla parece fazer o melhor.

No entanto, a biblioteca da Mozilla foi escrita para ser executada dentro do navegador. Para usá-la com o node, existe o readability-node, que me colocou na desconfortável posição de ter que atrapalhar o meu caminho com o node. Mas deixa pra lá, eu sou corajoso o suficiente e você também. Vamos em frente e usar a readability-node para construir a nossa ferramenta e tirar um pouco da carga do readability.org.

Escrevendo o app

A principal funcionalidade é bastante simples. Readability-node espera um DOM, que pode ser fornecido via jsdom, por isso vamos usar essa biblioteca também. Use cd para atualizar o diretório do projeto e execute estes comandos:

$ echo '{"name": "Readability-clone"}'
$ npm install readability-node jsdom --save

Depois disso, comece a editar seus app.js e vamos tentar fazer isso rodar. A primeira coisa que temos a fazer é descobrir como obter o código-fonte de uma url arbitrária. Aqui está o que eu pensei:

https = require('https');
r = require('readability-node');
jsdom = require('jsdom').jsdom;

https.get("https://adambard.com/blog/the-web-is-a-mature-platform/", function(res){
    var src = '';
    res.on('data', function(d){ src += d; });
    res.on('end', function(){
        console.log(src);
    });
});

Note que temos que acompanhar todas as nossas operações IO vagamente com esses callbakcs, em vez de usar funções síncronas e esperar, como em uma linguagem civilizada. Isso ocorre porque o node.js prioriza non-blockingness acima de tudo, especialmente sanidade.

Você pode rodar esse código com $ node app.js para ver se ele funciona como esperado.

Agora que temos o fonte, precisamos alimentá-lo através de readability-node via jsdom.

https = require('https');
r = require('readability-node');
jsdom = require('jsdom').jsdom;

var uri = "https://adambard.com/blog/the-web-is-a-mature-platform/";
https.get(uri, function(res){

    var src = '';
    res.on('data', function(d){ src += d; });
    res.on('end', function(){
        var doc = jsdom(src, {features: {
            FetchExternalResources: false,
            ProcessExternalResources: false
        }});
        var article = new r.Readability(uri, doc).parse();
        console.log(article.title, "\n\n", article.content);
    });
});

Assim, a parte robusta do app está pronta. O resto é apenas a criação de uma interface de usuário para usar um uri dinâmico e exibir bem o resultado. Vamos em frente e fazer um servidor que irá mostrar o resultado e, enquanto isso, comece a buscar a URL da cadeia de consulta quando possível.

url = require('url');
http = require('http');
https = require('https');
r = require('readability-node');
jsdom = require('jsdom').jsdom;

function handleRequest(req, resp){
    var uri = url.parse(req.url, true).query.url
    uri = uri || "https://adambard.com/blog/the-web-is-a-mature-platform/";

    https.get(uri, function(res){

        var src = '';
        res.on('data', function(d){ src += d; });
        res.on('end', function(){
            var doc = jsdom(src, {features: {
                FetchExternalResources: false,
                ProcessExternalResources: false
            }});
            var article = new r.Readability(uri, doc).parse();
            resp.write(
                "<html><head><meta charset='utf-8'><title>"
                + article.title
                + "</title></head><body>"
                + article.content
                + "</body></html>");
        });
    });
}

var server = http.createServer(handleRequest);

server.listen(process.env.PORT || 3000, function(){ console.log("OK");});

Você pode executar o aplicativo e apontar seu navegador para http://localhost:3000/ para ver o seu servidor rodando em toda a sua glória. Em algum momento, vamos querer alguns templates. Eu gosto de mustache, então vamos usar isso e criar um template dir:

$ npm install mu2 --save
$ mkdir templates

Agora podemos editar templates/index.html conforme o necessário. Aqui está o que fiz:

<html>
    <head>
        <meta charset="UTF-8">
        <title>{{title}}</title>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Libre+Baskerville">
        <link rel="stylesheet" href="/css/style.css">
    </head>
    <body>
        <header>
            <h1><a href="/">Fungibility</a></h1>
            <form method="get" action="/">
                <label for="url">Enter URL:</label>
                <input type="text" name="url">
                <button>Load</button>
            </form>
        </header>
        <main>
            <div class="container">
                <h2>{{title}}</h2>
                {{{content}}}
            </div>
        </main>
    </body>
</html>

Agora, nós temos um problema: precisamos de uma maneira para servir /css/style.css. Mas, já que estamos realmente meio-assing essa coisa toda, vamos servi-la a partir do Node:

url = require('url');
path = require('path');
fs = require('fs');
http = require('http');
https = require('https');
r = require('readability-node');
jsdom = require('jsdom').jsdom;
mu = require('mu2');
mu.root = __dirname + '/templates';

function render(resp, ctx){
    // A helper function to render index.html, our only template
    mu.compileAndRender('index.html', ctx).pipe(resp);
}


function serveFile(filename, resp){
    fs.readFile(filename, "binary", function(err, file) {
      if(err) {
        resp.writeHead(500, {"Content-Type": "text/plain"});
        resp.write(err + "\n");
        resp.end();
        return;
      }

      resp.writeHead(200);
      resp.write(file, "binary");
      resp.end();
    });
}

function serveReadability(uri, resp){
    uri = uri || "https://adambard.com/blog/the-web-is-a-mature-platform/";

    https.get(uri, function(res){

        var src = '';
        res.on('data', function(d){ src += d; });
        res.on('end', function(){
            var doc = jsdom(src, {features: {
                FetchExternalResources: false,
                ProcessExternalResources: false
            }});
            var article = new r.Readability(uri, doc).parse();
            render(resp, article);
        });
    });

}

function handleRequest(req, resp){
    var req_url = url.parse(req.url, true);
    var filename = path.join(process.cwd(), req_url.pathname);
    var uri = req_url.query.url;

    fs.exists(filename, function(exists) {
        if(exists && !fs.statSync(filename).isDirectory()){
            serveFile(filename, resp);
        }else{
            serveReadability(uri, resp);
        }
    });
}

var server = http.createServer(handleRequest);

server.listen(process.env.PORT || 3000, function(){ console.log("OK");});

Você pode (re)iniciar o servidor e ver que agora tem um layout relativamente bom para o seu app. Assim, a linha de base está praticamente pronta; vou deixar os ajustes e o css como um exercício para o leitor (apenas crie um arquivo /css/style.css no diretório do seu projeto e edite-o, e ele deve funcionar).

Fazendo o deploy do Heroku

Esta parte também é fácil. Tenha certeza de ter o mais recente heroku toolbelt, então

$ git init
$ echo 'web: node app.js' > Procfile
$ echo 'node_modules' > .gitignore
$ git add . && git commit -m "Initial commit"
$ heroku create
$ git push heroku master

Navegue até a url fornecida pelo Heroku, e você deve ver algo surgir. É simples assim!

E se eu quiser apenas colocar meu clone?

Eu chamei meu projeto de fungibility e coloquei-o no GitHub para qualquer um que queira usá-lo (ele também tem um pouco de tratamento de erros):

$ git clone https://github.com/adambard/fungibility.git
$ cd fungibility
$ heroku create
$ git push heroku master

Isso deve funcionar bem. Ou você pode apenas usar fungibility diretamente.

***

Adam Bard faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://adambard.com/blog/self-hosted-readability-on-heroku-with-nodejs/