Microlink를 사용하여 Rails에서 링크 미리 보기 생성

49081 단어 railstutorial
이 튜토리얼에서는 Microlink API를 활용하여 Ruby on Rails에서 링크 미리보기를 생성하는 방법을 보여드리겠습니다.


  • Source Code
  • Orignal Article

  • 1단계: 애플리케이션 설정


  • rails new rails-microlink-example -d=postgresql --webpacker=stimulus
  • rails db:create
  • rails db:migrate

  • 2단계: 링크 스캐폴드 생성


  • rails g scaffold link url:string
  • 데이터베이스 수준에서 null 값을 방지합니다.

  • class CreateLinks < ActiveRecord::Migration[6.1]
      def change
        create_table :links do |t|
          t.string :url, null: false
    
          t.timestamps
        end
      end
    end
    


  • rails db:migrate
  • 검증을 추가합니다.

  • class Link < ApplicationRecord
        validates :url, presence: true
    end
    


    3단계. 링크 테이블에 meta_data 열 추가


  • rails add_meta_data_to_link meta_data:jsonb
  • rails db:migrate
  • ActiveRecord::Store을 사용하여 meta_data 열에 저장된 데이터를 직렬화합니다. 이러한 접근자를 원하는 대로 호출할 수 있지만 Microlink response에서 반환된 동일한 이름을 사용합니다.

  • class Link < ApplicationRecord
        store :meta_data, accessors: [ :description, :image, :title  ], coder: JSON
        validates :url, presence: true
    end
    


    We could create a separate column for each accessor, but using ActiveRecord::Store allows for greater flexibly and keeps the database simple.


  • link_params 열의 값을 포함하도록 meta_data를 업데이트합니다.

  • class LinksController < ApplicationController
     ...
     private
        ...
        def link_params
          params.require(:link).permit(:url, :description, :image, :title)
        end
    end
    


    4단계: Microlink 설치 및 구성


  • yarn add @microlink/mql
  • touch app/javascript/controllers/microlink_controller.js

  • import { Controller } from "stimulus"
    import mql from "@microlink/mql"
    
    export default class extends Controller {
    }
    


    If you were you run rails s and view the console, you would see the following error:




  • babel.config.js를 추가하여 sourceType: "unambiguous"를 업데이트합니다.

  • module.exports = function(api) {
      ...  
      return {
        sourceType: "unambiguous",
        presets: [
            ...
        ].filter(Boolean),
        ...
      }
    }
    


    I found this solution be searching for the error and came across these resources:



    5단계: 숨겨진 필드에 API 응답 저장


  • app/views/links/_form.html.erb에서 마크업을 업데이트합니다.

  • <%= form_with(model: link, data: { controller: "microlink" }) do |form| %>
      ...
      <div class="field">
        <%= form.label :url %>
        <%= form.url_field :url, data: { microlink_target: "input", action: "change->microlink#handleChange" } %>
      </div>
    
      <%= form.hidden_field :description, data: { microlink_target: "descriptionInput" } %>
      <%= form.hidden_field :image, data: { microlink_target: "imageInput" } %>
      <%= form.hidden_field :title, data: { microlink_target: "titleInput" } %>
      ...
    <% end %>
    


  • handleChange에서 app/javascript/controllers/microlink_controller.js 메서드를 빌드합니다.

  • import { Controller } from "stimulus"
    import mql from "@microlink/mql"
    
    export default class extends Controller {
        static targets = [ "input", "descriptionInput", "imageInput", "titleInput" ]
    
        async handleChange() {
            const { status, data } = await mql(this.inputTarget.value)
            if(status == "success") {
                this.setFormData(data);
            }
        }
    
        setFormData(data) {
            this.descriptionInputTarget.value   = data?.description ? data?.description : null;
            this.imageInputTarget.value         = data?.image?.url ? data?.image?.url : null;
            this.titleInputTarget.value         = data?.title ? data?.title : null;
        }
    }
    


    Now when a user enters a URL, the hidden fields will be set with the response from the Microlink API.





    6단계: 링크 미리보기 렌더링


  • app/views/links/_preview.html.erb 부분을 만듭니다.

  • <div data-microlink-target="output" style="<%= @link.persisted? ? nil : 'display: none;' %>">
        <img src="<%= @link.persisted? ? @link.image : nil %>"/>
        <div>
            <h5><%= @link.persisted? ? @link.title : nil %></h5>
            <p><%= @link.persisted? ? @link.description : nil %></p>
        </div>
    </div>
    


  • 부분을 app/views/links/_form.html.erb에 추가합니다.

  • <%= form_with(model: link, data: { controller: "microlink" }) do |form| %>
      ...
      <%= render "preview" %>  
    <% end %>
    


  • 부분을 app/views/links/show.html.erb에 추가합니다.

  • <p id="notice"><%= notice %></p>
    
    <p>
      <strong>Url:</strong>
      <%= @link.url %>
    </p>
    
    <%= link_to @link.url, target: "_blank" do %>
      <%= render "preview" %>
    <% end %>
    
    <%= link_to 'Edit', edit_link_path(@link) %> |
    <%= link_to 'Back', links_path %>
    


  • renderPreview에서 app/javascript/controllers/microlink_controller.js 메서드를 빌드합니다.

  • import { Controller } from "stimulus"
    import mql from "@microlink/mql"
    
    export default class extends Controller {
        static targets = [ "input", "descriptionInput", "imageInput", "titleInput", "output" ]
    
        connect() {
            this.previewDescription = this.outputTarget.querySelector("p");
            this.previewImage       = this.outputTarget.querySelector("img");
            this.previewTitle       = this.outputTarget.querySelector("h5");
        }
    
        async handleChange() {
            const { status, data } = await mql(this.inputTarget.value)
            if(status == "success") {
                this.setFormData(data);
                this.renderPreview(data);
            }
        }
    
        renderPreview(data) {
            this.previewDescription.innerHTML = data?.description ? data.description : null;
            data?.image?.url ? this.previewImage.setAttribute("src", data.image.url) : null;
            this.previewTitle.innerHTML = data?.title ? data.title : null;
            this.outputTarget.style.display = "block";
        }
    
        setFormData(data) {
            this.descriptionInputTarget.value   = data?.description ? data.description : null;
            this.imageInputTarget.value         = data?.image?.url ? data.image.url : null;
            this.titleInputTarget.value         = data?.title ? data.title : null;
        }
    }
    


    At this point you should be able to render a link preview.



    .

    7단계: 미리보기 이미지를 링크에 첨부



    지금은 실제로 이미지를 Link에 첨부하는 것이 아니라 절대 URL을 이미지에 저장하고 있습니다. 이는 우리가 제어할 수 없기 때문에 시간이 지남에 따라 해당 이미지가 깨질 수 있음을 의미합니다. 한 가지 해결책은 이미지를 다운로드하고 Active Storage을 사용하여 Link에 첨부하는 것입니다.
  • rails active_storage:installrails db:migrate를 실행하여 Active Storage를 설치합니다.
  • 링크 모델에 has_one_attached :thumbnail를 추가합니다.

  • class Link < ApplicationRecord
        store :meta_data, accessors: [ :description, :image, :title  ], coder: JSON
        validates :url, presence: true
        has_one_attached :thumbnail
    end
    


  • bundle add down를 실행하여 down gem을(를) 설치합니다. 이렇게 하면 기본 Ruby를 사용하여 수행하는 것보다 Microlink API에서 반환된 원격 이미지를 더 쉽게 다운로드할 수 있습니다.
  • rails g job microlink_image_attacher를 실행하여 Active Job을 생성합니다. 이 작업을 사용하여 Microlink API에서 반환된 이미지를 다운로드하고 첨부합니다.

  • require "down"
    
    class MicrolinkImageAttacherJob < ApplicationJob
      queue_as :default
      discard_on Down::InvalidUrl
    
      def perform(link)
        if link.image.present?
          tempfile = Down.download(link.image)
          link.thumbnail.attach(io: tempfile, filename: tempfile.original_filename) 
        end
      end
    end
    


    We add discard_on Down::InvalidUrl to discard any job that returns a Down::InvalidUrl exception. This can happen is the Microlink API returns a base64 image.


  • 링크가 저장되면 MicrolinkImageAttacherJob을 수행합니다.

  • class LinksController < ApplicationController
      ...
      def create
        @link = Link.new(link_params)
    
        respond_to do |format|
          if @link.save
            MicrolinkImageAttacherJob.perform_now(@link)
            ...
          else
            ...
          end
        end
      end
    
    end
    


    You could call perform_later instead of perform_now.


  • app/views/links/_preview.html.erb에서 첨부된 축소판을 렌더링합니다.

  • <div data-microlink-target="output" style="<%= @link.persisted? ? nil : 'display: none;' %>">
        <img src="<%= @link.thumbnail.attached? ? url_for(@link.thumbnail) : nil %>"/>
        <div>
            <h5><%= @link.persisted? ? @link.title : nil %></h5>
            <p><%= @link.persisted? ? @link.description : nil %></p>
        </div>
    </div> 
    


    이제 이미지를 반환하는 링크를 저장하면 활성 저장소에 저장됩니다.

    8단계: 오류 처리 및 UX 개선



    이제 행복한 길을 완성했으므로 모든 오류를 설명하기 위해 UX를 개선해야 합니다. 특히 누군가 잘못된 URL을 입력하거나 Microlink API가 오류를 반환하는 경우입니다.
  • app/views/links/_form.html.erb에 메시지를 렌더링하기 위한 마크업을 추가합니다.

  • <%= form_with(model: link, data: { controller: "microlink" }) do |form| %>
      ...
      <div class="field">
        <%= form.label :url %>
        <%= form.url_field :url, data: { microlink_target: "input", action: "change->microlink#handleChange" } %>
        <span data-microlink-target="message"></span>
      </div>
      ...
      <%= render "preview" %>
    <% end %>
    


  • 오류를 처리하고 메시지를 렌더링하도록 업데이트app/javascript/controllers/microlink_controller.js합니다.

  • import { Controller } from "stimulus"
    import mql from "@microlink/mql"
    
    export default class extends Controller {
        static targets = [ "input", "descriptionInput", "imageInput", "titleInput", "output", "message" ]
    
        connect() {
            this.previewDescription = this.outputTarget.querySelector("p");
            this.previewImage       = this.outputTarget.querySelector("img");
            this.previewTitle       = this.outputTarget.querySelector("h5");
        }
    
        async handleChange() {
            this.messageTarget.innerText = null;
            this.clearFormData();
            this.clearPreview();
            if (this.inputTarget.value != "") {
                try {
                    const { status, data } = await mql(this.inputTarget.value)
                    this.messageTarget.innerText = "Fetching link preview...";
                    if(status == "success") {
                        this.setFormData(data);
                        this.renderPreview(data);
                        this.messageTarget.innerText = null;
                    } else {
                        this.messageTarget.innerText = "There was an error fetching the link preview.";
                    }
                } catch(e) {
                    this.messageTarget.innerText = e;
                }
            }
        }
    
        clearFormData() {
            this.descriptionInputTarget.value   = null;
            this.imageInputTarget.value         = null;
            this.titleInputTarget.value         = null;
        }
    
        clearPreview() {
            this.previewDescription.innerHTML   = null;
            this.previewImage.setAttribute("src", "");
            this.previewTitle.innerHTML         = null;
            this.outputTarget.style.display     = "none";
        }
    
        renderPreview(data) {
            this.previewDescription.innerHTML = data?.description ? data.description : null;
            data?.image?.url ? this.previewImage.setAttribute("src", data.image.url) : null;
            this.previewTitle.innerHTML = data?.title ? data.title : null;
            this.outputTarget.style.display = "block";
        }
    
        setFormData(data) {
            this.descriptionInputTarget.value   = data?.description ? data.description : null;
            this.imageInputTarget.value         = data?.image?.url ? data.image.url : null;
            this.titleInputTarget.value         = data?.title ? data.title : null;
        }
    }
    





    이 게시물이 마음에 드셨나요? 더 많은 팁을 얻으려면.

    좋은 웹페이지 즐겨찾기