개발

Froala Editor 를 이용한 오픈 그래프 링크 삽입

233 views as of November 1, 2024.
네이버나 티스토리같은 블로그 에디터에서는 에디터를 통해서 링크를 넣으면 일반 하이퍼 링크 방식이 아니라 해당 링크의 오픈 그래프 정보를 불러와서 박스 형태로 링크를 제작해준다.

또 카카오톡과 인스타그램, 페이스북에서도 마찬가지로 링크입력을 받으면 아래 처럼 오픈그래프 정보를 바탕으로 카드 링크를 제공해주기도 한다.

예시 1 - 카카오톡

예시 2 - 네이버 블로그

이런 오픈그래프 링크는 해당 링크를 직접 방문하지 않아도 대략적으로 사이트 정보를 알 수 있게하며 특히 오픈그래프 링크의 경우에는 og:image 를 통해 페이지의 컨텐츠를 대략적으로 볼 수 있다는 큰 장점이 있다.

단 이런 오픈그래프 링크는 html의 경우에는 레이아웃에 맞게 직접 생성해야하므로 실질적으로 에디터에서 이런 기능을 지원하지 않으면 사용하기 어렵다는 단점이 있다.

그래서 이런 부분에 대해서 직접 구현해보자 생각이 들었고 현재 라라벨 환경에서 플로라 에디터를 쓰는 상황에서의 개발 일지를 남기고자 한다.


필수 패키지 설치

일단 오픈 그래프 정보를 백단에서 얻어와야하므로 몇가지 패키지가 필요하다.

composer require guzzlehttp/guzzle
composer require ext-domCopy

나 같은 경우에는 Laravel 6.x 버전을 사용하므로 Illuminate\Support\Facades\Http 파사드가 내장되어있지 않아서 guzzle\Client 를 쓰게되었다.


Fetch 메소드 작성

다음으로는 링크가 주어지면 그 링크를 타고 가서 오픈그래프 정보를 빼오고 그걸 json으로 반환하는 메소드를 아래와 같이 작성한다.

public function fetchOG(Request $request)
{
    $url = $request->input('url');

    if (empty($url)) {
        return response()->json(['error' => 'URL is required'], 400);
    }

    try {
        // Guzzle로 HTML 데이터 가져오기
        $client = new Client();
        $response = $client->get($url);
        $htmlContent = $response->getBody()->getContents();

        // HTML 인코딩 확인 및 UTF-8로 변환
        $encoding = mb_detect_encoding($htmlContent, ['UTF-8', 'ISO-8859-1', 'EUC-KR', 'SJIS'], true);
        if ($encoding !== 'UTF-8') {
            $htmlContent = mb_convert_encoding($htmlContent, 'UTF-8', $encoding);
        }

        // DOMDocument로 HTML 파싱
        $dom = new DOMDocument();
        @$dom->loadHTML('<?xml encoding="UTF-8">' . $htmlContent); // UTF-8 설정 추가

        $xpath = new DOMXPath($dom);

        // Open Graph 데이터 기본 구조
        $ogData = [
            'title' => '',
            'description' => '',
            'image' => '',
            'url' => $url,
        ];

        // 요청 URL에서 호스트 추출
        $parsedUrl = parse_url($url);
        $host = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];

        // Open Graph 태그 추출
        $metaTags = $xpath->query("//meta[starts-with(@property, 'og:')]");
        foreach ($metaTags as $meta) {
            $property = $meta->getAttribute('property');
            $content = $meta->getAttribute('content');

            switch ($property) {
                case 'og:title':
                    $ogData['title'] = htmlspecialchars(html_entity_decode($content, ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8');
                    break;
                case 'og:description':
                    // HTML 엔터티로 변환하여 <iframe> 등이 순수 텍스트로 표시되도록 처리
                    $ogData['description'] = htmlspecialchars(html_entity_decode($content, ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8');
                    break;
                case 'og:image':
                    // 이미지 URL이 상대 경로라면 절대 경로로 변환
                    $imageUrl = $this->getAbsoluteUrl($content, $host);

                    // 이미지 URL 유효성 확인
                    if ($this->isValidImage($imageUrl)) {
                        $ogData['image'] = $imageUrl;
                    }
                    break;
            }
        }

        // og:title이 없는 경우 <title> 태그에서 가져오기
        if (empty($ogData['title'])) {
            $titleTag = $xpath->query("//title");
            if ($titleTag->length > 0) {
                $ogData['title'] = html_entity_decode($titleTag->item(0)->nodeValue, ENT_QUOTES, 'UTF-8');
            }
        }

        // og:description이 없는 경우 <meta name="description"> 태그에서 가져오기
        if (empty($ogData['description'])) {
            $metaDescription = $xpath->query("//meta[@name='description']");
            if ($metaDescription->length > 0) {
                $ogData['description'] = html_entity_decode($metaDescription->item(0)->getAttribute('content'), ENT_QUOTES, 'UTF-8');
            }
        }

        return response()->json($ogData);
    } catch (\Exception $e) {
        return response()->json(['error' => 'Failed to fetch Open Graph data'], 500);
    }
}Copy


만약에 위에서 언급한 Http 파사드를 이용한다면 URL 리퀘스트 부분(위 코드에서 라인 10-13)을 아래처럼 바꿔서 쓰면된다.

$response = Http::get($url);
$htmlContent = $response->body();Copy

본 컨트롤러의 역할은 다음과 같다.

1. Http 파사드나 Client를 이용해서 url에 request를 한 후 결과 값을 받는다. (한글 처리를 위해 XML을 UTF-8 형식으로 변경)
2. 우선적으로 OG(오픈 그래프) 데이터를 추출한 후 JSON 객체에 저장한다.
3. 만약에 객체가 비어있다면 일반 메타 태그를 추출한 후 저장한다.


Froala Editor 이벤트 추가

다음으로는 url fetch 요청을 하고 결과를 받아 실제로 오픈그래프 링크를 그리는 파트를 구현해보자.
나는 플로라 에디터의 이벤트 핸들러에다가 작성했지만 원리만 알면 어느 에디터에도 연동 가능할것이다.

...
 
events: {
    ...
 
    'paste.before': function (event) {
        const editor = this;
        const pastedContent = event.clipboardData.getData('text');
    
        // URL 형식인지 확인
        const urlPattern = /^(http|https):\/\/[^ "]+$/;
        if (urlPattern.test(pastedContent)) {
            event.preventDefault(); // 기본 붙여넣기 동작 방지
    
            // API 요청으로 Open Graph 데이터 가져오기
            fetch('/editor/fetch/og', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
                },
                body: JSON.stringify({url: pastedContent})
            })
                .then(response => response.json())
                .then(data => {
                    if (data.error) {
                        console.error(data.error);
                        return;
                    }
    
                    // Open Graph 데이터를 에디터에 표시할 HTML로 변환
                    const previewHtml = `
                        <div class="og_preview" data-url="${data.url}">
                            <div class="grp_image" style="background-image: url(${data.image})">
                                <img src="${data.image}" alt="${data.title}" class="og_image">
                            </div>
                            <div class="grp_og">
                                <a href="${data.url}" target="_blank" class="og_title link">${data.title}</a>
                                <div class="og_description">${data.description}</div>
                                <div class="og_url">${data.url}</div>
                            </div>
                        </div>
                    `;
    
                    // 에디터에 HTML 삽입
                    editor.html.insert(previewHtml);
                })
                .catch(error => {
                    console.error('Error fetching Open Graph data:', error);
                });
        }
    }
}Copy

이벤트 흐름은 다음과 같다.
1. 에디터에 붙여넣기 이벤트 발생시
2. 붙여넣는 데이터가 URL 형식을 만족할때
3. API 요청으로 만들어놓은 백단 메소드를 실행 시켜 오픈그래프 데이터를 취득 (이미지 검증에 대해서는 본문 하단의 추가기능 파트 참고)
4. 이후 오픈그래프 링크를 구조화하고 데이터를 넣은후 삽입


결과

이런식으로 프론트단과 백단에 필요한 라우팅작업까지 완료하고난 후에 스타일을 지정하고 링크를 에디터에 붙여넣기 해보면 아래와 같은 오픈그래프 링크를 생성할 수 있다.

Froala Editor  이미지 매니저 - eruLabo
Froala Editor 이미지 매니저 - eruLabo
Froala Editor는 직관적이고 다양한 기능을 제공하는 웹 기반 WYSIWYG 에디터예요. 이 에디터는 스크립트를 활용해서 내부 기능을 자유롭게 조율하거나 수정할 수 있어서, 사용자 목적에 맞는 맞춤형 에디터를 만들...
https://erulabo.com/190

네이버
네이버
네이버 메인에서 다양한 정보와 유용한 컨텐츠를 만나 보세요
https://www.naver.com

KANG BLOG
KANG BLOG
개발자로서의 개발을 위한 개발생활
https://kagrin97-blog.vercel.app/next/OpenGraphPreview

뭐 필요에 따라서 설명문 노출을 결정하거나 사이즈 조절, 링크 조절등의 추가 작업을 진행하면 커스텀 링크도 충분히 이런식으로 구현할 수 있지 않을까 싶다.


추가기능 - 이미지 검증

오픈 그래프 링크에 쓰이는 이미지가 접근 가능한지에 대해서 이미지 검증을 하는 코드를 백단에 아래와 같이 추가할 수 있다.


이미지 요청 및 확인

이미지 URL이 전달되면 그 URL에 Request 를 보내 이미지의 존재유무를 아래와 같이 검증 할 수 있다.

// 이미지가 유효한지 확인하는 메서드
private function isValidImage($url)
{
    try {
        $client = new Client();
        $response = $client->head($url); // HEAD 요청으로 이미지의 유효성만 확인
 
        // 상태 코드가 200이고 Content-Type이 image로 시작하는 경우에만 유효한 이미지로 판단
        return $response->getStatusCode() === 200 &&
            strpos($response->getHeaderLine('Content-Type'), 'image') === 0;
    } catch (\Exception $e) {
        return false; // 요청 실패 시 유효하지 않음으로 처리
    }
}
Colored by Color ScripterCopy


이미지 상대 경로 -> 절대 경로

그런데 여기서 대부분의 사이트의 경우에는 og:image 경로를 상대경로로 작성해놓는 이슈가 있다.
그럼 우리는 여기서 한술 더떠 해당 이미지 경로가 상대경로면 절대경로, 즉 이미지경로에 호스트를 붙여서 검증 요청을 붙여야 할것이다.

그러기위해서 우선 절대경로를 가져오기위한 메소드를 아래와 같이 만든다.

// URL이 상대 경로라면 호스트를 추가하여 절대 경로로 변환하는 메서드
private function getAbsoluteUrl($url, $host)
{
    // URL이 이미 절대 경로라면 그대로 반환
    if (parse_url($url, PHP_URL_SCHEME) !== null) {
        return $url;
    }
 
    // 상대 경로라면 호스트를 앞에 붙여 절대 경로로 변환
    return rtrim($host, '/') . '/' . ltrim($url, '/');
}Copy

그리고 여기에 넣을 파라미터를 fetchOG 메소드에 추가 해준다.

// 요청 URL에서 호스트 추출
$parsedUrl = parse_url($url);
$host = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];Copy


HTML 랜더링 구문 조정

이 리턴값을 대응하는 스크립트 생성코드부분을 다시 구현하면 아래와 같이 구현할 수 있겠다.

// Open Graph 데이터를 에디터에 표시할 HTML로 변환
const previewHtml = `
    <div class="og_preview" data-url="${data.url}">
        ${data.image ? `<div class="grp_image" style="background-image: url(${data.image})">
            <img src="${data.image}" alt="${data.title}" class="og_image">
        </div>` : '' }
        <div class="grp_og">
            <a href="${data.url}" target="_blank" class="og_title link">${data.title}</a>
            <div class="og_description">${data.description}</div>
            <div class="og_url">${data.url}</div>
        </div>
    </div>
`;Copy

${data.image ? `이미지 태그` : ''}Copy

이런식으로 이미지 태그에 해당하는 부분에 조건문을 걸어놓으면 오픈그래프 이미지 링크가 있으면 해당 태그가 적히고 없으면 적히지안게되어 아래처럼 적절히 링크 레이아웃을 제어할 수 있을 것이다.


#개발 #Froala Editor #플로라 에디터 #오픈 그래프 #Open Graph #링크
0 개의 댓글
×