재개발ㅋ 중인 모듈이 콘텐츠(article)를 생산하는 성격이 있다보니 SEO를 시도해보고 있습니다.

(람보님의 조언 : https://xetown.com/tips/1529234#comment_1530284 )

 

다음과 같이 코드를 짜서 얼추 성공한 것 같네요. :)

 

다만, 기존의 og 이미지 기입하는 방식이 document 모듈의 첨부파일을 가져오는 방식이다보니 약간 불만스러운 부분도 있었는데요.
그래서 이번에 작업하면서는 썸네일 생성 함수를 참조해서 og 이미지에 넣어봤습니다.

이렇게 하니 외부 이미지로 썸네일을 만들었어도 og 태그에는 외부 이미지 주소 그대로 나가게 됩니다. 유후~

 

한 가지 염려스러운 것은 서드파티에서 og 메타태그를 넣어준 뒤 코어의 HTMLDisplayHandler에서도 한번 더 작업을 하다보니, 두 번째 파일 소스의 하이라이트 처리된 부분(936~945행) 때문에 og:image 속성이 중복적으로 기입된다는 것인데요.

트리거가 제공되면 어떻게 끼어들어볼까 했는데 여의치가 않네요 하핫;;

그래도 다행스럽게 1) 메타 태그가 중복만 될 뿐 덮어써지지는 않는 것 같고, 2) 페이스북, 트위터 등에 링크 공유할 때도 아티클의 대표 이미지로 잘 잡히는 것 같습니다(다른 소셜 미디어나 링크 프리뷰에서는 어떻게 될지...).

 

아무튼 기록 삼아 코드 남겨봅니다ㅎ

 

schedule.view.php의 콘텐트뷰 액션 부분

// add the schedule title to the browser
Context::setCanonicalURL(getFullUrl('', 'mid', $this->module_info->mid, 'schedule_srl', $oSchedule->schedule_srl));
$seo_title = config('seo.document_title') ? config('seo.document_title') : '$SITE_TITLE - $DOCUMENT_TITLE';
Context::setBrowserTitle($seo_title, array(
    'site_title' => Context::getSiteTitle(),
    'site_subtitle' => Context::getSiteSubtitle(),
    'subpage_title' => $this->module_info->browser_title,
    'document_title' => $oSchedule->get('title'),
));

// Add OpenGraph and Twitter metadata
if ( config('seo.og_enabled') )
{
    $oSchedule->_addOpenGraphMetadata(); // schedule.item.php로 넘어갑니다.
    if ( config('seo.twitter_enabled') )
    {
        $oSchedule->_addTwitterMetadata(); // schedule.item.php로 넘어갑니다.
    }
}

 

schedule.item.php의 관련 메소드 부분

/**
 * Add OpenGraph metadata tags.
 * 
 * @return void
 */
function _addOpenGraphMetadata()
{
    // Get information about the current request.
    $page_type = 'website';
    $current_module_info = Context::get('current_module_info');
    $site_module_info = Context::get('site_module_info');
    $schedule_srl = Context::get('schedule_srl');
    $grant = Context::get('grant');
    $permitted = $grant->access;
    if ( isset($grant->view) && !$grant->view ) // 콘텐트 뷰 권한 확인
    {
        $permitted = false;
    }
    if ( $schedule_srl && $permitted )
    {
        if ( isset($grant->private) && !$grant->private && $current_module_info->use_private === 'Y' ) // private : 자체 설정한 비밀 스케줄러 권한 확인
        {
            $permitted = false;
        }
        else
        {
            if ( is_object($this) && $this->schedule_srl == $schedule_srl )
            {
                $page_type = 'article';
                if ( $this->get('status') == 'STANDBY' && $current_module_info->standby_display != 'SHOW' ) // 대기 상태의 스케줄 콘텐츠인지 확인
                {
                    $permitted = false;
                }
            }
        }
    }

    // Add basic metadata. -> 제목과 사이틀 이름 부분은 중복되기 때문에 주석 처리
    // Context::addOpenGraphData('og:title', $permitted ? Context::getBrowserTitle() : lang('msg_not_permitted'));
    // Context::addOpenGraphData('og:site_name', Context::getSiteTitle());
    if ( $page_type === 'article' && $permitted && config('seo.og_extract_description') )
    {
        $description = trim(utf8_normalize_spaces($this->getContentText(200)));
    }
    else
    {
        $description = Context::getMetaTag('description');
    }
    Context::addOpenGraphData('og:description', $description);
    Context::addMetaTag('description', $description);

    // Add metadata about this page.
    Context::addOpenGraphData('og:type', $page_type);
    if ( $page_type === 'article' )
    {
        $canonical_url = getFullUrl('', 'mid', $current_module_info->mid, 'schedule_srl', $schedule_srl);
    }
    elseif ( ($page = Context::get('page')) > 1 )
    {
        $canonical_url = getFullUrl('', 'mid', $current_module_info->mid, 'page', $page);
    }
    elseif ( $current_module_info->module_srl == $site_module_info->module_srl )
    {
        $canonical_url = getFullUrl('');
    }
    else
    {
        $canonical_url = getFullUrl('', 'mid', $current_module_info->mid);
    }
    Context::addOpenGraphData('og:url', $canonical_url);
    Context::setCanonicalURL($canonical_url);

    // Add metadata about the locale.
    $lang_type = Context::getLangType();
    $locales = (include \RX_BASEDIR . 'common/defaults/locales.php');
    if ( isset($locales[$lang_type]) )
    {
        Context::addOpenGraphData('og:locale', $locales[$lang_type]['locale']);
    }

    // Add image.
    if ( $document_images = Context::getMetaImages() )
    {
        // pass
    }
    elseif ( $page_type === 'article' && $permitted && config('seo.og_extract_images') )
    {
        if ( ($document_images = Rhymix\Framework\Cache::get("seo:document_images:$schedule_srl")) === null )
        {
            // 여기서부터 썸네일 생성 소스에서 응용
            $source_file = null;
            $is_tmp_file = false;
            $uploaded_count = FileModel::getFilesCount($schedule_srl);
            $content = $this->get('content');

            // Find an image file among attached files if exists
            if ( $uploaded_count ) // 첨부 파일 존재하는 경우
            {
                $file_list = FileModel::getFiles($schedule_srl, array(), 'file_srl', true);
                $first_image = null;

                foreach ( $file_list as $file )
                {
                    if ( $file->thumbnail_filename && file_exists($file->thumbnail_filename) )
                    {
                        $file->uploaded_filename = $file->thumbnail_filename;
                    }
                    else
                    {
                        if ( $file->direct_download !== 'Y' || !preg_match('/\.(jpe?g|png|gif|webp|bmp)$/i', $file->source_filename) )
                        {
                            continue;
                        }
                        if ( !file_exists($file->uploaded_filename) )
                        {
                            continue;
                        }
                    }
                    if ( $file->cover_image === 'Y' )
                    {
                        $source_file = $file->uploaded_filename;
                        break;
                    }
                    if ( !$first_image )
                    {
                        $first_image = $file->uploaded_filename;
                    }
                }

                if ( !$source_file && $first_image )
                {
                    $source_file = $first_image;
                }
                if ( $source_file )
                {
                    list($width, $height) = @getimagesize($source_file);
                    if ($width < 100 && $height < 100)
                    {
                        $source_file = null;
                    }
                }
            }

            // 첨부파일 업이 이미지가 콘텐츠 안에 삽입되어 있는 경우 : 즉 외부 이미지!!!
            $config = DocumentModel::getDocumentConfig();
            if ( !$source_file && $config->thumbnail_target !== 'attachment' )
            {
                preg_match_all("!<img\s[^>]*?src=(\"|')([^\"' ]*?)(\"|')!is", $content, $matches, PREG_SET_ORDER);
                foreach ( $matches as $match )
                {
                    $target_src = htmlspecialchars_decode(trim($match[2]));
                       if ( preg_match('/\/(common|modules|widgets|addons|layouts)\//i', $target_src) )
                    {
                        continue;
                    }
                    else
                    {
                        if ( !preg_match('/^https?:\/\//i',$target_src) )
                        {
                            $target_src = Context::getRequestUri().$target_src;
                        }

                        $tmp_file = sprintf('./files/cache/tmp/%d', md5(rand(111111,999999).$schedule_srl));
                        if ( !is_dir('./files/cache/tmp') )
                        {
                            FileHandler::makeDir('./files/cache/tmp');
                        }
                        FileHandler::getRemoteFile($target_src, $tmp_file);
                        if ( !file_exists($tmp_file) )
                        {
                            continue;
                        }
                        else
                        {
                            if ( $is_img = @getimagesize($tmp_file) )
                            {
                                list($width, $height) = $is_img; // 외부이미지의 가로 세로 사이즈를 가져옴
                                if ($width < 100 && $height < 100)
                                {
                                    continue;
                                }
                            }
                            else
                            {
                                continue;
                            }
                            $source_file = $target_src;
                            $is_tmp_file = true;
                            break;
                        }
                    }
                }

                // Remove source file if it was temporary
                if ( $is_tmp_file )
                {
                    FileHandler::removeFile($tmp_file);
                }
            }

            if ( $source_file )
            {
                $document_images[] = array('filepath' => $source_file, 'width' => $width, 'height' => $height);
            }
            Rhymix\Framework\Cache::set("seo:document_images:$schedule_srl", $document_images);
        }
    }
    else
    {
        $document_images = null;
    }

    if ( $document_images )
    {
        $first_image = array_first($document_images);
        if ( !preg_match('/(^http?:\/\/)|(^https?:\/\/)/i', $first_image['filepath']) )
        {
            $first_image['filepath'] = str_replace('./', '', Context::getRequestUri() . $first_image['filepath']);
        }
        $first_image['filepath'] = preg_replace('/^.\\/files\\//', \RX_BASEURL . 'files/', $first_image['filepath']);
        Context::addOpenGraphData('og:image', $first_image['filepath']);
        Context::addOpenGraphData('og:image:width', $first_image['width']);
        Context::addOpenGraphData('og:image:height', $first_image['height']);
        $this->_image_type = 'document';
    }
    elseif ($default_image = getAdminModel('admin')->getSiteDefaultImageUrl($site_module_info->domain_srl, $width, $height))
    {
        Context::addOpenGraphData('og:image', Rhymix\Framework\URL::getCurrentDomainURL($default_image));
        if ( $width && $height )
        {
            Context::addOpenGraphData('og:image:width', $width);
            Context::addOpenGraphData('og:image:height', $height);
        }
        $this->_image_type = 'site';
    }
    else
    {
        $this->_image_type = 'none';
    }

    // Add datetime for articles.
    if ( $page_type === 'article' && $permitted && config('seo.og_use_timestamps') )
    {
        Context::addOpenGraphData('og:article:published_time', $this->getRegdate('c'));
    }
}

/**
 * Add Twitter metadata tags.
 * 
 * @return void
 */
function _addTwitterMetadata()
{
    $card_type = $this->_image_type === 'document' ? 'summary_large_image' : 'summary';
    Context::addMetaTag('twitter:card', $card_type);

    foreach(Context::getOpenGraphData() as $val)
    {
        if ($val['property'] === 'og:title')
        {
            Context::addMetaTag('twitter:title', $val['content']);
        }
        if ($val['property'] === 'og:description')
        {
            Context::addMetaTag('twitter:description', $val['content']);
        }
        if ($val['property'] === 'og:image' && $this->_image_type === 'document')
        {
            Context::addMetaTag('twitter:image', $val['content']);
        }
    }
}

 

 

 

윤삼

profile
아무래도 중급 초반 수준의 코딩 오타쿠인 것 같습니다.
  • profile

    라이믹스 최신 버전 기준 Context::addMetaImage() 메소드를 사용하여 이미지를 지정하면 HTMLDisplayHandler에서 별도로 이미지를 추가하지 않으므로 중복으로 출력되지 않습니다.

     

    https://github.com/rhymix/rhymix/blob/master/classes/context/Context.class.php#L2696

     

    그 밖에도 서드파티 자료에서 SEO 데이터를 좀더 쉽게 조작할 수 있도록 지원하는 방안을 연구해 보겠습니다. 지금 쓰는 방식은 예전 XE의 SEO 모듈에서 쓰던 방식을 그대로 코어에 이식한 거라, 게시판 위주로만 만들어져 있어요.

  • profile profile

    알려주신 메소드는 외부 이미지는 튕겨내네요ㅜ
    내부 이미지는 Context::getMetaImages()로 보면 추가가 된 것 같은데 제가 코드 삽입 위치를 잘못 잡았는지 모르겠지만 메타태그의 og 쪽으로는 전달되지 않는 것 같습니다. 이건 저도 연구를 좀..
    일단 그냥 가던대로 가면서ㅋ 연구하시는 거 열심히 팔로업하겠습니다 ㅎㅎ

  • profile
    저희 사이트 같은경우

    https://snsdstagram.com/taeyeon_ss/dispGginstagramContentview?gginstagram_srl=184930

    주소로 접속하면 해당 내용이 주소창 및 카카오톡링크를 통해서도 나오는데 이 부분을 사실 SEO Pro모듈에서 해결했다는게...(속닥속닥..)

    모듈자체에서 SEO제공해주면 더 좋은데 라이믹스에 기진님께서 말씀하신 것처럼 별도의 자료조작을 할 수 있는 방법이 지원된다면 이 부분에서 좀 더 해결이 쉬워질 것 같네요. :)
  • profile profile
    네, 막상 구현하고 나니까 나중에 보면 알아보기 힘들 정도로 코드가 너무 길어졌어요ㅜㅜ

    외부 이미지를 긁어서 따로 저장을 해두셨군요.
    근데 저처럼 외부 이미지 주소를 바로 메타 태그에 넣어두면 상도의에 좀 어긋나려나요...
    일단 각종 링크 프리뷰 들에는 잘 나오긴 합니다만ㅎ