By Sails.js Tech Brasil

Conversão de Embedded Javascript Templating (EJS) para PUG Render

Em termos gerais, Sails.js se utiliza da tecnologia EJS para renderizar no backend e entregar páginas HTML5 + CSS + JavaScript dinâmicas ao Cliente Web.

No entanto, o Sails.js é versátil o suficiente para permitir que você altere este padrão para qualquer renderizador de sua preferência.

Um renderizador de páginas age no backend fazendo a conversão de seu conteúdo formatado em HTML antes de enviar ao Navegador, ou seja, para o navegador do cliente não faz a menor diferença se seu renderizador é um EJS, Pug, Jade, Java Server Faces ou qualquer outro. Quem necessita conhecer e converter os padrões de template em html é sua API, ´rodando´ no lado Servidor.

Comparando um template EJS com sua versão Pug:

EJS


<div id="account-overview" v-cloak>

  <div class="container pt-5 pb-5">
    <h1>My account</h1>
    <hr/>
    <div class="row mb-3">
      <div class="col-sm-6">
        <h4>Personal information</h4>
      </div>
      <div class="col-sm-6">
        <span class="float-sm-right">
          <a style="width: 150px" class="btn btn-sm btn-outline-info" href="/account/profile">Edit profile</a>
        </span>
      </div>
    </div>
    <div class="row">
      <div class="col-3">Name:</div>
      <div class="col"><strong>{{me.fullName}}</strong></div>
    </div>
    <div class="row">
      <div class="col-3">Email:</div>
      <div class="col">
        <strong :class="[me.emailStatus === 'unconfirmed' || me.emailStatus === 'change-requested' ? 'text-muted' : '']">{{me.emailChangeCandidate ? me.emailChangeCandidate : me.emailAddress}}</strong>
        <span v-if="me.emailStatus === 'unconfirmed' || me.emailStatus === 'change-requested'" class="badge badge-pill badge-warning">Unverified</span>
      </div>
    </div>
    <hr/>
    <div class="row mb-3">
      <div class="col-sm-6">
        <h4>Password</h4>
      </div>
      <div class="col-sm-6">
        <span class="float-sm-right">
          <a style="width: 150px" class="btn btn-sm btn-outline-info" href="/account/password">Change password</a>
        </span>
      </div>
    </div>
    <div class="row">
      <div class="col-3">Password:</div>
      <div class="col"><strong>••••••••••</strong></div>
    </div>
    <hr/>
    <div class="row mb-3" v-if="isBillingEnabled">
      <div class="col-sm-6">
        <h4>Billing</h4>
      </div>
      <div class="col-sm-6">
        <span class="float-sm-right">
          <button style="width: 150px;" class="btn btn-sm btn-outline-info" @click="clickUpdateBillingCardButton()">{{ me.hasBillingCard ? 'Change card' : '+ Add card' }}</button>
        </span>
      </div>
    </div>
    <div v-if="isBillingEnabled && me.hasBillingCard">
      <div class="row">
        <div class="col-3">Credit card:</div>
        <div class="col">{{me.billingCardBrand}} ending in <strong>{{me.billingCardLast4}}</strong> <a style="text-decoration: underline; cursor: pointer;" class="ml-2" purpose="remove-button" @click="clickRemoveCardButton()">Remove</a></div>
      </div>
      <div class="row">
        <div class="col-3">Expiration:</div>
        <div class="col">{{me.billingCardExpMonth}}/{{me.billingCardExpYear}}</div>
      </div>
    </div>
    <div class="alert alert-danger" v-else-if="isBillingEnabled && cloudError">
      There was an error updating your credit card information. Please check your information and try again, or <a href="/contact">contact support</a> if the error persists.
    </div>
    <div class="alert alert-secondary" v-else-if="isBillingEnabled">
      You have not linked a payment source to your account. In order to access paid features, you'll need to provide your credit card information. (Don't worry: you will only be charged when you've reached the limit of your free plan.)
    </div>
  </div>
              

pug

#account-overview(v-cloak='')
  account-notification-banner
  .container.pt-5.pb-5
    h1 My account
    hr
    .row.mb-3
      .col-sm-6
        h4 Personal information
      .col-sm-6
        span.float-sm-right
          a.btn.btn-sm.btn-outline-info(style='width: 150px' href='/account/profile') Edit profile
    .row
      .col-3 Name:
      .col
        strong {{me.fullName}}
    .row
      .col-3 Email:
      .col
        strong(:class="[me.emailStatus === 'unconfirmed' || me.emailStatus === 'change-requested' ? 'text-muted' : '']") {{me.emailChangeCandidate ? me.emailChangeCandidate : me.emailAddress}}
        span.badge.badge-pill.badge-warning(v-if="me.emailStatus === 'unconfirmed' || me.emailStatus === 'change-requested'") Unverified
    hr
    .row.mb-3
      .col-sm-6
        h4 Password
      .col-sm-6
        span.float-sm-right
          a.btn.btn-sm.btn-outline-info(style='width: 150px' href='/account/password') Change password
    .row
      .col-3 Password:
      .col
        strong ••••••••••
    hr
    .row.mb-3(v-if='isBillingEnabled')
      .col-sm-6
        h4 Billing
      .col-sm-6
        span.float-sm-right
          button.btn.btn-sm.btn-outline-info(style='width: 150px;' @click='clickUpdateBillingCardButton()') {{ me.hasBillingCard ? 'Change card' : '+ Add card' }}
    div(v-if='isBillingEnabled && me.hasBillingCard')
      .row
        .col-3 Credit card:
        .col
          | {{me.billingCardBrand}} ending in
          strong {{me.billingCardLast4}}
          a.ml-2(style='text-decoration: underline; cursor: pointer;' purpose='remove-button' @click='clickRemoveCardButton()') Remove
      .row
        .col-3 Expiration:
        .col {{me.billingCardExpMonth}}/{{me.billingCardExpYear}}
    .alert.alert-danger(v-else-if='isBillingEnabled && cloudError')
      | There was an error updating your credit card information. Please check your information and try again, or
      a(href='/contact') contact support
      |  if the error persists.
    .alert.alert-secondary(v-else-if='isBillingEnabled')
      | You have not linked a payment source to your account. In order to access paid features, you'll need to provide your credit card information. (Don't worry: you will only be charged when you've reached the limit of your free plan.)

              
É claro que nem todas as ferramentas desenvolvidas pelo Time Sails dentro do EJS funcionarão no pug, mas esta será uma troca justa se considerarmos a redução do tamanho dos arquivos e a simplicitade de manutenção futura proporcionadas pela tecnologia pug!

Se esta é sua primeira vez aqui, recomendamos a leitura dos conteúdos abaixo antes de continuar:


Pré-requisitos

Primeira Etapa: Instalação da biblioteca PUG/Jade (1 min)
  1. Instalação do Pug
    npm i pug --save
  2. Localize o arquivo config/views.js e informe ao Sails que ele usará o pug ao invés do EJS e qual será a função de renderização a ser utilizada para converter os templates em html.
    
    module.exports.views = {
    
      /***************************************************************************
      *                                                                          *
      * Extension to use for your views. When calling `res.view()` in an action, *
      * you can leave this extension off. For example, calling                   *
      * `res.view('homepage')` will (using default settings) look for a          *
      * `views/homepage.pug` file.                                               *
      *                                                                          *
      ***************************************************************************/
    
      extension: 'pug',
    
      getRenderFn: ()=>{
    
      var cons = require('pug');
      // Return the rendering function for Pug.
      return cons.renderFile;
    },
    
    
    
      /***************************************************************************
      *                                                                          *
      * The path (relative to the views directory, and without extension) to     *
      * the default layout file to use, or `false` to disable layouts entirely.  *
      *                                                                          *
      * Note that layouts only work with the built-in EJS view engine!           *
      *                                                                          *
      ***************************************************************************/
    
      layout: false
    
    };
    
                
  3. A partir deste ponto o Sails não será mais capaz de localizar os arquivos com extensão .ejs, pois estará procurando por arquivos com a extensão .pug

Segunda Etapa: Substituição dos arquivos (15 min)
  1. Localize as versões *.ejs existentes no seu protótipo e crie uma cópia *.pug para cada
  2. Agora o Sails será capaz de localizar os arquivos, mas o renderizador pug não reconhecerá o template como válido para pug, LOGO, VOCÊ PRECISARÁ REESCREVÊ-LOS!
  3. Como exemplo, seguem abaixo os dois primeiros aquivos utilizados pelo Sails para exposição do protótipo, são eles:
    • Arquivo base de layout views/layouts/layout.pug
      
      doctype html
      html
        head
          title NEW_APP_NAME
          meta(
            content="width=device-width, initial-scale=1, maximum-scale=1"
            name="viewport"
          )
          meta(
            content="noindex"
            name="robots"
          )
          link(
            href="/dependencies/bootstrap-4/bootstrap-4.css"
            rel="stylesheet"
          )
          link(
            href="/dependencies/fontawesome.css"
            rel="stylesheet"
          )
          link(
            href="/styles/importer.css"
            rel="stylesheet"
          )
      
        body
          div
            header.navbar.navbar-expand-sm.navbar-dark.bg-dark.flex-column.flex-md-row.justify-content-between
              a.navbar-brand.mr-0(
                href="/"
                style="cursor: pointer;"
              )
                img.logo(
                  src="/images/logo.png"
                  alt="NEW_APP_NAME logo"
                  style="height: 20px;"
                )
              .navbar-nav.flex-row
                // LOGGED-IN NAVIGATION
                |  if(me) {
                a.nav-item.nav-link.ml-2.ml-md-0.mr-2.mr-md-0(href="/contact") Help
                // Only in desktop nav
                .nav-item.dropdown.d-none.d-sm-block
                  a#header-account-menu-link.nav-link.dropdown-toggle(
                    aria-expanded="false"
                    aria-haspopup="true"
                    data-toggle="dropdown"
                  ) Account
                  .dropdown-menu(
                    aria-labelledby="header-account-menu-link"
                    style="left: auto; right: 0;"
                  )
                    a.dropdown-item(href="/account") Settings
                    a.dropdown-item(href="/logout") Sign out
                // Only in mobile nav
                a.nav-item.nav-link.ml-2.mr-2.d-block.d-sm-none(href="/account") Account Settings
                a.nav-item.nav-link.ml-2.mr-2.d-block.d-sm-none(href="/logout") Sign out
                |  } else {
                // LOGGED-OUT NAVIGATION
                a.nav-item.nav-link.ml-2.ml-md-0.mr-2(href="/faq") FAQ
                a.nav-item.nav-link.ml-2.ml-md-0.mr-2(href="/login") Log in
                // Only in desktop nav
                .form-inline.d-none.ml-2.d-md-block
                  a.btn.btn-outline-info(href="/signup") Sign up
                // Only in mobile nav
                a.nav-item.nav-link.text-info.ml-2.d-block.d-md-none(href="/signup") Sign up
                |  }
            // Alert if email has not been confirmed
            if me&&me.emailChangeCandidate
              .container-fluid
                .alert.alert-secondary.mt-2.small(role="alert")
                  | Your updated email address needs verification. Until you click the link sent to
                  strong = me.emailChangeCandidate
                  | , you'll still need to sign in as
                  strong = me.emailAddress
                  | .
            else if me&&me.emailStatus === 'unconfirmed'
              .container-fluid
                .alert.alert-secondary.mt-2.small(role="alert")
                  | Your email address still needs verification. Your account access may be limited until you click the link sent to
                  strong = me.emailChangeCandidate ? me.emailChangeCandidate : me.emailAddress
                  | .
      
          block content
            footer.navbar.navbar-light.justify-content-between.flex-row-reverse(purpose="page-footer")
              .nav(purpose="footer-nav")
                small.nav-item.d-inline-block
                  a.nav-link.text-info.px-1.px-sm-3(href="/contact")
                    | Contact
                    span.d-none.d-sm-inline us
                small.nav-item.d-inline-block
                  a.nav-link.text-info.px-1.px-sm-3(href="/legal/terms")
                    | Terms
                    span.d-none.d-sm-inline of use
                small.nav-item.d-inline-block
                  a.nav-link.text-info.px-1.px-sm-3(href="/legal/privacy")
                    | Privacy
                    span.d-none.d-sm-inline policy
                |  if(me) {
                small.nav-item.d-inline-block
                  a.nav-link.text-info.px-1.px-sm-3(href="/logout") Sign out
                |  }
              small(purpose="footer-copy")
                | Copyright © = sails.config.custom.platformCopyrightYear
                a(
                  href="NEW_APP_COMPANY_ABOUT_HREF"
                  target="_blank"
                ) NEW_APP_COMPANY_NAME
                | .
                br.d-block.d-sm-none
                | All rights reserved.
      
      
          //  /*
          //   Client-side JavaScript
          //   ========================
          //  Scripts can be hard-coded as «script» tags, automatically injected
          //|   by the asset pipeline between "SCRIPTS" and "SCRIPTS END", or both.
          //|   (https://sailsjs.com/docs/concepts/assets/task-automation)
          //|   */
          //|   /* Stripe.js */
          script(src="https://js.stripe.com/v3/")
          //|  /* Delete the global `self` to help avoid client-side bugs.
          //|   (see https://developer.mozilla.org/en-US/docs/Web/API/Window/self) */
          script.
            delete window.self;
          //| /* bowser.js (for browser detection) -- included inline to avoid issues with minification that could affect the unsupported browser overlay */
          script.
            !function(e,i,s){if("undefined"!=typeof module&&module.exports)module.exports=s();else if("function"==typeof define&&define.amd)define(i,s);else e[i]=s()}(this,"bowser",function(){var e=true;function i(i){function s(e){var s=i.match(e);return s&&s.length>1&&s[1]||""}function o(e){var s=i.match(e);return s&&s.length>1&&s[2]||""}var r=s(/(ipod|iphone|ipad)/i).toLowerCase(),n=/like android/i.test(i),t=!n&&/android/i.test(i),a=/nexus\s*[0-6]\s*/i.test(i),d=!a&&/nexus\s*[0-9]+/i.test(i),l=/CrOS/.test(i),f=/silk/i.test(i),m=/sailfish/i.test(i),v=/tizen/i.test(i),p=/(web|hpw)os/i.test(i),c=/windows phone/i.test(i),u=/SamsungBrowser/i.test(i),h=!c&&/windows/i.test(i),w=!r&&!f&&/macintosh/i.test(i),b=!t&&!m&&!v&&!p&&/linux/i.test(i),g=o(/edg([ea]|ios)\/(\d+(\.\d+)?)/i),k=s(/version\/(\d+(\.\d+)?)/i),x=/tablet/i.test(i)&&!/tablet pc/i.test(i),y=!x&&/[^-]mobi/i.test(i),S=/xbox/i.test(i),B;if(/opera/i.test(i))B={name:"Opera",opera:e,version:k||s(/(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i)};else if(/opr\/|opios/i.test(i))B={name:"Opera",opera:e,version:s(/(?:opr|opios)[\s\/](\d+(\.\d+)?)/i)||k};else if(/SamsungBrowser/i.test(i))B={name:"Samsung Internet for Android",samsungBrowser:e,version:k||s(/(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i)};else if(/coast/i.test(i))B={name:"Opera Coast",coast:e,version:k||s(/(?:coast)[\s\/](\d+(\.\d+)?)/i)};else if(/yabrowser/i.test(i))B={name:"Yandex Browser",yandexbrowser:e,version:k||s(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)};else if(/ucbrowser/i.test(i))B={name:"UC Browser",ucbrowser:e,version:s(/(?:ucbrowser)[\s\/](\d+(?:\.\d+)+)/i)};else if(/mxios/i.test(i))B={name:"Maxthon",maxthon:e,version:s(/(?:mxios)[\s\/](\d+(?:\.\d+)+)/i)};else if(/epiphany/i.test(i))B={name:"Epiphany",epiphany:e,version:s(/(?:epiphany)[\s\/](\d+(?:\.\d+)+)/i)};else if(/puffin/i.test(i))B={name:"Puffin",puffin:e,version:s(/(?:puffin)[\s\/](\d+(?:\.\d+)?)/i)};else if(/sleipnir/i.test(i))B={name:"Sleipnir",sleipnir:e,version:s(/(?:sleipnir)[\s\/](\d+(?:\.\d+)+)/i)};else if(/k-meleon/i.test(i))B={name:"K-Meleon",kMeleon:e,version:s(/(?:k-meleon)[\s\/](\d+(?:\.\d+)+)/i)};else if(c){B={name:"Windows Phone",osname:"Windows Phone",windowsphone:e};if(g){B.msedge=e;B.version=g}else{B.msie=e;B.version=s(/iemobile\/(\d+(\.\d+)?)/i)}}else if(/msie|trident/i.test(i))B={name:"Internet Explorer",msie:e,version:s(/(?:msie |rv:)(\d+(\.\d+)?)/i)};else if(l)B={name:"Chrome",osname:"Chrome OS",chromeos:e,chromeBook:e,chrome:e,version:s(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)};else if(/edg([ea]|ios)/i.test(i))B={name:"Microsoft Edge",msedge:e,version:g};else if(/vivaldi/i.test(i))B={name:"Vivaldi",vivaldi:e,version:s(/vivaldi\/(\d+(\.\d+)?)/i)||k};else if(m)B={name:"Sailfish",osname:"Sailfish OS",sailfish:e,version:s(/sailfish\s?browser\/(\d+(\.\d+)?)/i)};else if(/seamonkey\//i.test(i))B={name:"SeaMonkey",seamonkey:e,version:s(/seamonkey\/(\d+(\.\d+)?)/i)};else if(/firefox|iceweasel|fxios/i.test(i)){B={name:"Firefox",firefox:e,version:s(/(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i)};if(/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(i)){B.firefoxos=e;B.osname="Firefox OS"}}else if(f)B={name:"Amazon Silk",silk:e,version:s(/silk\/(\d+(\.\d+)?)/i)};else if(/phantom/i.test(i))B={name:"PhantomJS",phantom:e,version:s(/phantomjs\/(\d+(\.\d+)?)/i)};else if(/slimerjs/i.test(i))B={name:"SlimerJS",slimer:e,version:s(/slimerjs\/(\d+(\.\d+)?)/i)};else if(/blackberry|\bbb\d+/i.test(i)||/rim\stablet/i.test(i))B={name:"BlackBerry",osname:"BlackBerry OS",blackberry:e,version:k||s(/blackberry[\d]+\/(\d+(\.\d+)?)/i)};else if(p){B={name:"WebOS",osname:"WebOS",webos:e,version:k||s(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)};/touchpad\//i.test(i)&&(B.touchpad=e)}else if(/bada/i.test(i))B={name:"Bada",osname:"Bada",bada:e,version:s(/dolfin\/(\d+(\.\d+)?)/i)};else if(v)B={name:"Tizen",osname:"Tizen",tizen:e,version:s(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i)||k};else if(/qupzilla/i.test(i))B={name:"QupZilla",qupzilla:e,version:s(/(?:qupzilla)[\s\/](\d+(?:\.\d+)+)/i)||k};else if(/chromium/i.test(i))B={name:"Chromium",chromium:e,version:s(/(?:chromium)[\s\/](\d+(?:\.\d+)?)/i)||k};else if(/chrome|crios|crmo/i.test(i))B={name:"Chrome",chrome:e,version:s(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)};else if(t)B={name:"Android",version:k};else if(/safari|applewebkit/i.test(i)){B={name:"Safari",safari:e};if(k)B.version=k}else if(r){B={name:"iphone"==r?"iPhone":"ipad"==r?"iPad":"iPod"};if(k)B.version=k}else if(/googlebot/i.test(i))B={name:"Googlebot",googlebot:e,version:s(/googlebot\/(\d+(\.\d+))/i)||k};else B={name:s(/^(.*)\/(.*) /),version:o(/^(.*)\/(.*) /)};if(!B.msedge&&/(apple)?webkit/i.test(i)){if(/(apple)?webkit\/537\.36/i.test(i)){B.name=B.name||"Blink";B.blink=e}else{B.name=B.name||"Webkit";B.webkit=e}if(!B.version&&k)B.version=k}else if(!B.opera&&/gecko\//i.test(i)){B.name=B.name||"Gecko";B.gecko=e;B.version=B.version||s(/gecko\/(\d+(\.\d+)?)/i)}if(!B.windowsphone&&(t||B.silk)){B.android=e;B.osname="Android"}else if(!B.windowsphone&&r){B[r]=e;B.ios=e;B.osname="iOS"}else if(w){B.mac=e;B.osname="macOS"}else if(S){B.xbox=e;B.osname="Xbox"}else if(h){B.windows=e;B.osname="Windows"}else if(b){B.linux=e;B.osname="Linux"}function O(e){switch(e){case"NT":return"NT";case"XP":return"XP";case"NT 5.0":return"2000";case"NT 5.1":return"XP";case"NT 5.2":return"2003";case"NT 6.0":return"Vista";case"NT 6.1":return"7";case"NT 6.2":return"8";case"NT 6.3":return"8.1";case"NT 10.0":return"10";default:return undefined}}var T="";if(B.windows)T=O(s(/Windows ((NT|XP)( \d\d?.\d)?)/i));else if(B.windowsphone)T=s(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i);else if(B.mac){T=s(/Mac OS X (\d+([_\.\s]\d+)*)/i);T=T.replace(/[_\s]/g,".")}else if(r){T=s(/os (\d+([_\s]\d+)*) like mac os x/i);T=T.replace(/[_\s]/g,".")}else if(t)T=s(/android[ \/-](\d+(\.\d+)*)/i);else if(B.webos)T=s(/(?:web|hpw)os\/(\d+(\.\d+)*)/i);else if(B.blackberry)T=s(/rim\stablet\sos\s(\d+(\.\d+)*)/i);else if(B.bada)T=s(/bada\/(\d+(\.\d+)*)/i);else if(B.tizen)T=s(/tizen[\/\s](\d+(\.\d+)*)/i);if(T)B.osversion=T;var P=!B.windows&&T.split(".")[0];if(x||d||"ipad"==r||t&&(3==P||P>=4&&!y)||B.silk)B.tablet=e;else if(y||"iphone"==r||"ipod"==r||t||a||B.blackberry||B.webos||B.bada)B.mobile=e;if(B.msedge||B.msie&&B.version>=10||B.yandexbrowser&&B.version>=15||B.vivaldi&&B.version>=1||B.chrome&&B.version>=20||B.samsungBrowser&&B.version>=4||B.firefox&&B.version>=20||B.safari&&B.version>=6||B.opera&&B.version>=10||B.ios&&B.osversion&&B.osversion.split(".")[0]>=6||B.blackberry&&B.version>=10.1||B.chromium&&B.version>=20)B.a=e;else if(B.msie&&B.version<10||B.chrome&&B.version<20||B.firefox&&B.version<20||B.safari&&B.version<6||B.opera&&B.version<10||B.ios&&B.osversion&&B.osversion.split(".")[0]<6||B.chromium&&B.version<20)B.c=e;else B.x=e;return B}var s=i("undefined"!==typeof navigator?navigator.userAgent||"":"");s.test=function(e){for(var i=0;i=0)if(s[0][i]>s[1][i])return 1;else if(s[0][i]===s[1][i]){if(0===i)return 0}else return-1}function t(e,o,r){var t=s;if("string"===typeof o){r=o;o=void 0}if(void 0===o)o=false;if(r)t=i(r);var a=""+t.version;for(var d in e)if(e.hasOwnProperty(d))if(t[d]){if("string"!==typeof e[d])throw new Error("Browser version in the minVersion map should be a string: "+d+": "+String(e));return n([a,e[d]])<0}return o}function a(e,i,s){return!t(e,i,s)}s.isUnsupportedBrowser=t;s.compareVersions=n;s.check=a;s._detect=i;s.detect=i;return s});
          //|  /* Auto-injected «script» tags: */
          // SCRIPTS
          script(src="/dependencies/sails.io.js")
          script(src="/dependencies/lodash.js")
          script(src="/dependencies/jquery.min.js")
          script(src="/dependencies/vue.js")
          script(src="/dependencies/vue-router.js")
          script(src="/dependencies/bootstrap-4/bootstrap-4.bundle.js")
          script(src="/dependencies/cloud.js")
          script(src="/dependencies/moment.js")
          script(src="/dependencies/parasails.js")
          script(src="/js/cloud.setup.js")
          script(src="/js/components/account-notification-banner.component.js")
          script(src="/js/components/ajax-button.component.js")
          script(src="/js/components/ajax-form.component.js")
          script(src="/js/components/cloud-error.component.js")
          script(src="/js/components/js-timestamp.component.js")
          script(src="/js/components/modal.component.js")
          script(src="/js/components/stripe-card-element.component.js")
          script(src="/js/utilities/open-stripe-checkout.js")
          script(src="/js/pages/account/account-overview.page.js")
          script(src="/js/pages/account/edit-password.page.js")
          script(src="/js/pages/account/edit-profile.page.js")
          script(src="/js/pages/contact.page.js")
          script(src="/js/pages/dashboard/welcome.page.js")
          script(src="/js/pages/edit-tarefas.page.js")
          script(src="/js/pages/entrance/confirmed-email.page.js")
          script(src="/js/pages/entrance/forgot-password.page.js")
          script(src="/js/pages/entrance/login.page.js")
          script(src="/js/pages/entrance/new-password.page.js")
          script(src="/js/pages/entrance/signup.page.js")
          script(src="/js/pages/faq.page.js")
          script(src="/js/pages/homepage.page.js")
          script(src="/js/pages/legal/privacy.page.js")
          script(src="/js/pages/legal/terms.page.js")
          script(src="/js/pages/lista-tarefas.page.js")
          // SCRIPTS END
      
                         
    • Arquivo inicial Homepage views/pages/homepage.pug
      
      extends ../layouts/layout
      
      block content
      
        #homepage(v-cloak="").d-flex.flex-column.justify-content-center.position-relative(
          purpose="full-page-hero"
          style="padding-top: 100px; padding-bottom: 25px; color: #14acc2;"
        )
          .container.text-center
            .mx-auto.position-relative(style="width: 220px; height: 170px;")
              img.position-absolute(
                src="/images/hero-sky.png"
                alt="The sky above the ocean"
                style="width: 170px; left: 25px; top: 25px;"
              )
              img.position-absolute(
                src="/images/hero-cloud.png"
                alt="A grayish-blue cloud"
                purpose="cloud-1"
                style="width: 80px; top: 55px; left: -40px;"
              )
              img.position-absolute(
                src="/images/hero-cloud.png"
                alt="Another grayish-blue cloud"
                purpose="cloud-2"
                style="width: 80px; top: 45px; left: -40px;"
              )
              img.position-absolute(
                src="/images/hero-ship.png"
                alt="A ship hovering a few feet over the surface of the water, bearing a flagpole with the Sails.js logo"
                purpose="ship"
                style="width: 160px; bottom: 50px; left: 18px;"
              )
              img.position-absolute(
                src="/images/hero-water.png"
                alt="The shadow of the floating ship on the water"
                style="width: 170px; bottom: 40px; left: 25px;"
              )
            h1.display-4.pb-5
              | A new
              strong Sails
              | app.
            div
              .position-absolute.w-100.mt-5(
                @click="clickHeroButton()"
                purpose="more-info-text"
                style="cursor: pointer; bottom: 25px; left: 0;"
              )
                .text-uppercase.font-weight-bold(style="letter-spacing: 2px;") Dive in
                div(style="font-size: 20px;") ↓
        .text-center.py-5(
          purpose="scroll-destination"
          style="background-color: rgba(238, 245, 249, 0.72);"
        )
          .container.pt-3.pb-5(purpose="about-section")
            h3 This is your freshly-generated project + a few extras.
            p.mx-auto(style="max-width: 800px;")
              | In our time
              a(
                href="https://sailsjs.com/about"
                target="_blank"
              ) building apps for customers
              | , we've found ourselves re-rolling some of the same key features over and over again between projects.  So we picked some of the things that kept popping up, implemented our own opinionated solutions for them, and then included it all in this free and open-source starter app.  We hope it helps you as much as it helps us!
            .row.pt-5
              .mb-4.mb-sm-0.col-sm
                .rounded-circle.mx-auto.text-center.mb-3(
                  style="color: #fff; background-color: #14acc2; font-size: 35px; line-height: 75px; height: 75px; width: 75px;"
                )
                  i.fa.fa-envelope
                h4 Emails
                p.text-muted
                  | Built-in support for internal emails from the
                  a(href="/contact") contact form
                  | , as well as transactional emails for users.
              .pt-4.pt-sm-0.mb-4.mb-sm-0.col-sm
                .rounded-circle.mx-auto.text-center.mb-3(
                  style="color: #fff; background-color: #14acc2; font-size: 35px; line-height: 75px; height: 75px; width: 75px;"
                )
                  i.fa.fa-lock
                h4 Authentication
                p.text-muted
                  | Ready-to-go, customizable
                  a(href="/signup") sign up
                  | ,
                  a(href="/login") login
                  | , and
                  a(href="/password/forgot") password recovery
                  | flows for your users.
              .pt-4.pt-sm-0.col-sm
                .rounded-circle.mx-auto.text-center.mb-3(
                  style="color: #fff; background-color: #14acc2; font-size: 35px; line-height: 75px; height: 75px; width: 75px;"
                )
                  i.fa.fa-credit-card
                h4 Billing
                p.text-muted Hook up to your Stripe account for managing customers and payment sources.
            a.btn.btn-outline-info.mt-3(href="/faq") Learn more
        .container.pt-5
          .pt-5(purpose="setup-section")
            h3.text-center How to get started:
            .position-relative.my-5(purpose="setup-step")
              .position-absolute.d-none.d-lg-block(style="left: 0; top: 0; width: 140px;")
                img.w-100(
                  src="/images/setup-email.png"
                  alt="computer with email symbol on screen"
                )
              h5 Set up your branding and content
              p
                | This starter app includes several different page templates (including the one you're reading right now) that you can change as much or as little as you like.  If there are any pages you won't need, just remove them.
              p
                | To get started, do a global find/replace in this project for occurrences of
                code NEW_APP_NAME
                | and replace them all with the actual name of your app or platform (e.g. "Facebag").  Then do the same thing again to replace
                code NEW_APP_COMPANY_NAME
                | with the actual name of your organization (e.g. "Facebag Corporation").
              blockquote
                small
                  | This app also includes a default
                  a(href="/legal/terms") Terms of Use
                  | and
                  a(href="/legal/privacy") Privacy Policy
                  | .  We want to make it easier for apps to be transparent about their users' rights and privacy.  But we are developers, not lawyers; and this is
                  em definitely not legal advice
                  | .  Before going live, be sure to replace these example documents with your own company's policies, and
                  a(
                    target="_blank"
                    href="https://en.wikipedia.org/wiki/Counsel"
                  ) seek counsel
                  | for assistance if you need to design new terms from scratch.
            .position-relative.my-5(purpose="setup-step")
              .position-absolute.d-none.d-lg-block(style="left: 0; top: 0; width: 140px;")
                img.w-100(
                  src="/images/setup-payment.png"
                  alt="computer with credit card icon on screen"
                )
              h5 Configure integrations
              p
                | In order for this app to send automated emails, you'll need to create a
                a(
                  href="https://sendgrid.com/"
                  target="_blank"
                ) Sendgrid
                | account.  Then, in
                code config/custom.js
                | , configure the following:
              .card.bg-light.mb-4
                .card-body
                  pre
                    code
                      span.text-muted // Recipient of contact form messages
                      | internalEmailAddress: my.email@example.com,
                      span.text-muted // For outgoing emails
                      | fromEmailAddress: 'noreply@example.com',
                      | fromName: 'The NEW_APP_NAME Team',
                      span.text-muted // Sendgrid settings
                      | sendgridSecret: 'SG.fake.3e0Bn0qSQVnwb1E4qNPz9JZP5vLZYqjh7sn8S93oSHU'
              p
                | To enable support for billing features, you'll need to make a
                a(
                  href="https://stripe.com"
                  target="_blank"
                ) Stripe
                | account, then include your test credentials like so:
              .card.bg-light.mb-4
                .card-body
                  pre
                    code
                      span.text-muted // Stripe credentials
                      | stripePublishableKey: 'pk_test_Zzd814nldl91104qor5911gjald',
                      | stripeSecret: 'sk_test_Zzd814nldl91104qor5911gjald'
              blockquote
                small
                  | If your app doesn't need payment processing, it will still work without Stripe configuration. In this case, all references to billing will just be omitted from the UI.
            .position-relative.my-5(purpose="setup-step")
              .position-absolute.d-none.d-lg-block(style="left: 0; top: 0; width: 140px;")
                img.w-100(
                  src="/images/setup-customize.png"
                  alt="computer with sails logo on screen"
                )
              h5 Customize
              p
                | Once the initial configuration is done, you're ready to start building out the rest of your app.
              p
                | We worked hard to make this starter app's structure consistent and its files are as bare-bones as possible, so it's easy to add new pages and business logic following the conventions we set up. If you run into trouble, have a look at
                a(href="/faq") your new FAQ page
                | , which covers the tools we used and how to customize.  For a deeper dive, check out the Sails
                a(href="https://courses.platzi.com/courses/sails-js/") walkthrough
                | ,
                a(href="https://sailsjs.com/documentation") reference documentation
                | and
                a(href="https://sailsjs.com/support") support
                | pages.
          hr
          .text-center.py-5(purpose="pep-talk")
            h3 Let's get to work.
            p.mx-auto(style="max-width: 800px;")
              | We think this project is a pretty convenient starting point, but of course there's no one-size-fits-all solution. The good news is, this app is in your hands now, so you can jump into the files and adapt it to your needs. Change some code around. Break stuff.  Fix it. And above all:
              strong make something people want to use
              | .
            p.mb-5
              span.text-muted ♥
              a.border-0(href="https://sailsjs.com/about") The Sails Team
      
      section !{exposeLocalsToBrowser()}
      
                          
  4. Se você fez as configurações corretamente e utilizou nossos exemplos acima, então você deverá ver a seguinte tela
    homepage pug
  5. Recomendamos que, para conversão inicial dos arquivos gerados pelo Sails como ejs para pug, você utilize o site Pugify que é capaz de converter html (sem tratar as tags EJS) em pug, fazendo os ajustes necessários em seguida.
      Dicas aplicáveis aos ajustes:
    • a função <%=variavel %> deve ser substituída por #{variavel};
    • a função <%=__('chave de traducao i18n') %> deve ser substituída por span #{__('chave de traducao 118n')};
    • o comando <% if(teste) { %> deve ser substituída por if teste, utilizando identacao para o conteúdo que estava entre chaves; e
    • templates (também chamados componentes) incorporados tais como <account-notification-banner></account-notification-banner> devem ser substituidos pelo componente pug block content ou pelo componente include assets/js/components/account-notification-banner.component.js, conforme o caso.
  6. Achou interessante? Consulte a Documentação do pug em Inglês ou visite o Pug Cheatsheet para se familiarizar com o "modo pug de codifição de templates"

Para saber mais:
Início