How to Create a Link Preview: The Definite Guide [Implementation and Demo Included]

When you add a link in a chat message or share it on social networks like Facebook, Twitter, LinkedIn, you can see a small preview and a short description of the link. The main benefit of link previews is that users have some expectation of what they will get before opening the link.

In this blog post, we will create a solution, which converts a link:

In this:

I released this solution as npm packaged. You can check the source code on Github and the demo implemented on Heroku.

Nowadays, we can see the link preview feature in almost all social networks or chats. app, where users can send or share url links. In this blog post, I want to share with you how to create a link preview function without a third-party API. I’m going to describe the entire strategy of creating link previews, including implementation using open source libraries in node.js.

Why did I decide to write this blog post?

When I needed to create a preview link function, I came across a lot of misleading or outdated information on this topic. If I found a solution that worked, it was based on some paid 3rd party APIs. I hope this article saves you a lot of time figuring out how to build this function with open source libraries in any back-end language.

What should be included in a link preview?

A URL link preview usually contains the title, a description, the domain name, and an image. You can create even richer link previews by providing other information. For more details, see Additional Tips.

How to get data to preview a link?

Facebook launched the Open Graph protocol in 2010, which is now managed by Open Web Foundation. The main goal is an easier integration between Facebook and other websites. That being said, Open Graph Protocol allows you to control what information is used when sharing a website. If websites want to use the Open Graph Protocol, they must have Open Graph meta tags in the part of the website’s code.

og meta tags

Other social networks also take Open Graph Protocol into account. However, Twitter created its own tags for Twitter Cards, which are called Twitter Card Tags. They are based on the same conventions as the Open Graph protocol. When the Twitter card processor looks for tags on a page, it first looks for the Twitter-specific property, and if it’s not present, it falls back to the supported Open Graph property. More information can be found in the Twitter documentation.

 twitter meta tags

The following Open Graph tags are used to create link previews:

Open Graph Title

This tag works the same as the . Allows you to define the title of the content. If Facebook can’t find the og:title tag, use the meta instead. This tag is very important, because <title> is usually displayed in bold.</p> <p>There is no limit on the number of characters, but the title must be between 60 and 90 characters as a meta title. Otherwise, it may be shortened or truncated. For example, Facebook will truncate it to 88 characters.</p> <p> <b>Open Chart Description</b> </p> <p>This tag is again similar to the meta tag description. This is where the content of the website is described. Similar rules apply to this tag as for the title tag. If a social media bot can’t find the og:description tag, it uses a meta description, and there’s no limit to the number of characters. In this case, you should use around 200 letters.</p> <p> <b>Open Graph Image</b> </p> <p>An image is probably the most eye-catching element in the link preview. You can define the image with og:image title. The recommended resolution is 1200 pixels x 627 pixels (1.91/1 aspect ratio) and the image size should not exceed 5 MB.</p> <div style="clear:both; margin-top:0em; margin-bottom:1em;"><a href="https://httl.com.vn/en/how-to-connect-oculus-quest-2-to-roku-tv/" target="_self" rel="nofollow" class="u4b7ff586ec76a27a445db2ae04ad2f5b"><!-- INLINE RELATED POSTS 1/3 //--><style> .u4b7ff586ec76a27a445db2ae04ad2f5b { padding:0px; margin: 0; padding-top:1em!important; padding-bottom:1em!important; width:100%; display: block; font-weight:bold; background-color:#e6e6e6; border:0!important; border-left:4px solid #F1C40F!important; text-decoration:none; } .u4b7ff586ec76a27a445db2ae04ad2f5b:active, .u4b7ff586ec76a27a445db2ae04ad2f5b:hover { opacity: 1; transition: opacity 250ms; webkit-transition: opacity 250ms; text-decoration:none; } .u4b7ff586ec76a27a445db2ae04ad2f5b { transition: background-color 250ms; webkit-transition: background-color 250ms; opacity: 1; transition: opacity 250ms; webkit-transition: opacity 250ms; } .u4b7ff586ec76a27a445db2ae04ad2f5b .ctaText { font-weight:bold; color:#E74C3C; text-decoration:none; font-size: 16px; } .u4b7ff586ec76a27a445db2ae04ad2f5b .postTitle { color:#3498DB; text-decoration: underline!important; font-size: 16px; } .u4b7ff586ec76a27a445db2ae04ad2f5b:hover .postTitle { text-decoration: underline!important; } </style><div style="padding-left:1em; padding-right:1em;"><span class="ctaText">See Also:</span>  <span class="postTitle">How to connect oculus quest 2 to roku tv</span></div></a></div><p> <b>Open Graphic URL</b> </p> <p> This tag defines the canonical URL of your page. The URL provided is not displayed in the Facebook newsfeed, only the domain is displayed.</p> <p>You can find a full list of available og tags on the Open Graph website.</p> <h2>How to get data without metadata and og tags?</h2> <p>There are many websites without meta tags and basic og tags. What data should we preview in this case?</p> <p>We can use data in the body of the document.</p> <p> <b>The title</b> </p> <p>If the website does not contain a meta title tag or og: title tag, we can consider a heading in the document body as the main title. The most important heading in the body of the document is </p> <h1>. If the website does not contain the </p> <h1> tag, we can search for </p> <h2> tags.</p> <p> <b>The description</b> </p> <p>The strategy for getting the description of the website is similar to getting the title. . If the document does not contain a meta description or og:description, we can consider the main text of the document as the description of the website.</p> <p> <b>The domain name</b> </p> <p>We will search for or og:url. If the document doesn’t contain one of these, we’ll use the url parameter.</p> <p> <b>The image</b> </p> <p>Of all the attributes mentioned, the image is the most complicated element.</p> <p> <b>Which image should represent the URL of the website, if the document html doesn’t contain the og:image? tag</b> </p> <p>There is another way to specify the image of the website. There is a link tag with the rel=”image_src” attribute in the following format:</p> <p>However, we can find many websites without og : image or tag. In this case, we need to parse the images in the document body.</p> <p>Raymond Camden described in his 2011 blog post how Facebook and Google+ used to determine which image to use for link preview. Facebook used the og:image and tags and Google+ used the first <img> tag in the html body. Neither of these strategies seems correct, because Facebook did not consider images in the document body and Google+ chose the first image, which could be an image for the layout.</p> <p>Slack published a blog post, how do they create link previews, but do not take into account images in the html body.</p> <blockquote> <p><i>How does Facebook determine which images to display as thumbnails when posting a link?</i></p> </blockquote> <blockquote> <p><i>Candidate images are filtered using javascript which removes all images less than 50 pixels tall or wide and all images with a longest dimension to longest dimension ratio cut greater than 3:1. Leaked images are sorted by area, and users can choose whether multiple images exist.</i></p> </blockquote> <p><cite>quora.com</cite></p> <blockquote> <p><i>By removing the ability to customize link metadata (ie title, description, image) from all link sharing entry points on Facebook, we are removing a channel that has been abused for posting fake news .</i> </p> </blockquote> <p><cite>developers.facebook.com</cite></p> <p>I think the described strategy works well. Images less than 50px tall or wide are perhaps icons, images with an aspect ratio greater than 3:1 don’t fit well in previews. Images with a larger area are perhaps more important to website content than smaller images.</p> <h2>Implementation</h2> <p>You can find several attempts to create a library that implements the function of preview links.</p> <p>There is a node.js “fix” on AWS Lambda. Unfortunately, the main library and its source code repository are no longer available.</p> <blockquote> <p><i>Is there open source code for creating ‘link preview’ text and icons, such as on Facebook? </i></p> </blockquote> <p>stackoverflow.com</p> <p>I couldn’t find any open source implementations, so let’s build one.</p> <h3>Libraries used </h3> <p>If you want to implement the whole strategy for creating link previews, you should use a library that allows you to access the DOM structure of the html document. In the node.js environment, I found three libraries that allow you to access the DOM:</p> <ul> <li> JSDom simulates a web browser environment in node.js and allows you to access the DOM structure</li> <li> Puppeteer lets you control Chrome without a GUI from Node.js</li> <li>PhantomJS, a non-gui web browser scriptable with JavaScript</li> </ul> <p>JSDom doesn’t work ok, because we need to be visible url elements and JSDom doesn’t parse css styles well [1, 2].</p> <p>If you need to choose between Puppeteer and PhantomJS, I would recommend using Puppeteer, because PhantomJS development has stopped and Puppeteer is faster and requires less memory.</p> <h3>Configuring Puppeteer for Web Scraping</h3> <p>Puppeteer has many options and allows you to configure Chrome with various settings. Therefore, using Puppeteer for the first time is not that simple. Before you can open websites in Puppeteer, you must configure it to extract data from websites.</p> <p>Some websites do not want you to extract data. In this case, you can use puppeteer-extra-plugin-stealth, which uses various techniques to make it more difficult to detect a headless puppeteer.</p> <p>If you want to interact with the website in Puppeteer, you must use the Function page .evaluate(), where Puppeteer runs the script in the browser, not in node.js. If you have other modules or functions that you want to use in the evaluate function, you should use page.exposeFunction(). Modules imported into node.js are not accessible in the Puppeteer browser, and the expose function allows you to expose functions in the browser.</p> <p>When the browser makes a request to a website, it sends an HTTP header called “User Agent”. The user agent contains information about the web browser. Some websites do not provide meta tags for common user agents. In Puppeteer, you can configure the Facebook crawler user agent because, in most cases, websites want to provide metadata for Facebook.</p> <h3>Strategy for getting individual elements for link preview</h3> <p>We are going to implement the following strategy in node.js, which should be applicable in all back-end languages.</p> <p> <b>The title</b> </p> <p>Find og:title in the document header.If og:title does not exist, look for the meta title tag in the document head. If the meta title doesn’t exist, look for the </p> <h1> tag in the body of the document. If </p> <h1> does not exist, look for the first occurrence of the </p> <h2> tag in the document body.</p> <p> const getTitle = asynchronous page => { const title = await page.evaluate(() => { const ogTitle = document.querySelector(‘meta[property=”og:title”]’); if (ogTitle != null && ogTitle.content.length > 0) { return ogTitle.content; } const twitterTitle = document.querySelector(‘meta [ name=”twitter:title”]’); if (twitterTitle != null && twitterTitle.content.length > 0) { return twitterTitle.content; } const docTitle = document.title; if (docTitle != null && docTitle. length > 0) { return docTitle; } const h1 = document.querySelector(“h1”).innerHTML; if (h1 != null && h1.length > 0) { return h1; } const h2 = document.querySelector(“h1 ” ).innerHTML; if (h2 != null && h2.length > 0) { return h2; } return null; }); return title; }; </p> <p>Source: github.com</p> <p> <b>The description</b> </p> <p>Find og:description in the document header. If og:description doesn’t exist, look for the meta description tag in the document head. If the meta description tag doesn’t exist, parse the document body text. Finds the first visible paragraph, whose text is the site description.</p> <p> const getDescription = asynchronous page => { const description = expect page.evaluate(() => { const ogDescription = document.querySelector( ‘meta[property =”og :description”]’ ); if (ogDescription != null && ogDescription.content.length > 0) { return ogDescription.content; } const twitterDescription = document.querySelector( ‘meta[name=”twitter:description”] ‘ ); if (twitterDescription != null && twitterDescription.content.length > 0) { return twitterDescription.content; } const metaDescription = document.querySelector(‘meta[name=”description”]’); if (metaDescription != null && metaDescription.content.length > 0) { return metaDescription.content; } paragraphs = document.querySelectorAll(“p”); let fstVisibleParagraph = null; for (let i = 0; i < paragraphs.length; i++) { if ( // if object is visible in dom paragraphs[i].offsetParent !== null && !paragraphs[i].childElementCount != 0 ) { fstVisibleParag raph = paragraphs[i].textContent ; break; } } returns fstVisibleParagraph; }); return description; }; </p> <p>Source: github.com</p> <p> <b>The domain name</b> </p> <p>Find or og:url. If the document does not contain one of these, use the url parameter.</p> <p> const getDomainName = async (page, uri) => { const domainName = await page.evaluate(() => { const canonicalLink = document.querySelector ( “link[rel=canonical]”); if (canonicalLink != null && canonicalLink.href.length > 0) { return canonicalLink.href; } const ogUrlMeta = document.querySelector(‘meta[property=”og:url” ] ‘); if (ogUrlMeta != null && ogUrlMeta.content.length > 0) { return ogUrlMeta.content; } return null; }); return domain name! = null? new URL (domain name). hostname. replace(“www.”, “”): new url(uri). hostname. replace(“www.”, “”); }; </p> <p>Source: github.com</p> <p> <b>The Image</b> </p> <p>Find og:image in the document header. If og:image doesn’t exist, look for the tag in the header. If the tag does not exist, search for all images in the body of the document. Delete all images that are less than 50 pixels in height or width, and all images with a longest dimension to shortest dimension ratio greater than 3:1. Returns the image with the largest area.</p> <p> const util = require(“util”); const request = util.promisify(require(“request”)); const getUrls = require(“get-urls”); const urlImageIsAccessible = asynchronous url => { const correctedUrls = getUrls(url); if (correctedURL.size! == 0) { const urlResponse = wait for request(correctedURL.values().next().value); const contentType = urlResponse.headers[“content-type”]; returns new RegExp(“image/*”).test(contentType); } }; const getImg = async(page, uri) => { const img = expect page.evaluate(async() => { const ogImg = document.querySelector(‘meta[property=”og:image”]’); if ( ogImg != null && ogImg.content.length > 0 && (expect urlImageIsAccessible(ogImg.content)) ) { return ogImg.content; } const imgRelLink = document.querySelector(‘link[rel=”image_src”]’); if ( imgRelLink != null && imgRelLink.href.length > 0 && (expect urlImageIsAccessible(imgRelLink.href)) ) { return imgRelLink.href; } const twitterImg = document.querySelector(‘meta[name=”twitter:image”]’) if ( twitterImg != null && twitterImg.content.length > 0 && (expect urlImageIsAccessible(twitterImg.content)) ) { return twitterImg.content; } let imgs = Array.from(document.getElementsByTagName(“img”)); if (imgs.length > 0) { imgs = imgs.filter(img => { let addImg = true; if (img.naturalWidth > img.naturalHeight) { if (img.naturalWidth / img.naturalHeight > 3) { addImg = false ; } } else { if (img.naturalHeight / img.naturalWidth > 3) { addImg = false my; } } if (img.naturalHeight <= 50 || img.naturalwidth img.src.indexOf(“//”) === -1 ? (img.src = `${new URL(uri).source}/${src}`) : img. origin); return imgs[0].src; } returns null; }); return image; }; </p> <p>Source: github.com</p> <h2>Testing</h2> <p>If you want to test your link preview implementation, you can use the Facebook sharing debugger. This is a free tool, which scrapes any web page hosted on a public server and shows how it would look when shared.</p> <h2>Additional Tips</h2> <p>Your link previews can still be richer and provide more information to users. For example, if the website contains the og:video tag, you can replace the image with video. There is other information that you can use in the previews. There are specific tags for articles, books, or profiles.</p> <p>Consider setting up a proxy or using IP rotation for your server, as some websites try to detect web scraping and block it. Some websites block users from specific countries. If you need more tips to avoid web scraping detection, you can refer to this article.</p> <h2>Conclusion</h2> <p>In this article, we describe how social media and chat apps create previews of links. We then describe the implementation, which can be used in any back-end language. As an example, we implement the entire solution in node.js. The result is an open source node.js <b>library</b> and the demo is implemented on <b>Heroku</b>.</p> <p>As you can see, creating a preview function Link building is easy if you use the right approach. You don’t need to depend on third-party APIs and pay for similar services.</p> <p>.</p> </div> </div> </article> <div id="comments" class="comments-area"> <div id="respond" class="comment-respond"> <h3 id="reply-title" class="comment-reply-title">Leave a Reply <small><a rel="nofollow" id="cancel-comment-reply-link" href="/en/how-to-create-a-website-preview/#respond" style="display:none;">Cancel reply</a></small></h3><form action="https://httl.com.vn/en/wp-comments-post.php" method="post" id="commentform" class="comment-form" novalidate><p class="comment-notes"><span id="email-notes">Your email address will not be published.</span> <span class="required-field-message">Required fields are marked <span class="required">*</span></span></p><p class="comment-form-comment"><label for="comment">Comment <span class="required">*</span></label> <textarea id="comment" name="comment" cols="45" rows="8" maxlength="65525" required></textarea></p><p class="comment-form-author"><label for="author">Name <span class="required">*</span></label> <input id="author" name="author" type="text" value="" size="30" maxlength="245" autocomplete="name" required /></p> <p class="comment-form-email"><label for="email">Email <span class="required">*</span></label> <input id="email" name="email" type="email" value="" size="30" maxlength="100" aria-describedby="email-notes" autocomplete="email" required /></p> <p class="comment-form-url"><label for="url">Website</label> <input id="url" name="url" type="url" value="" size="30" maxlength="200" autocomplete="url" /></p> <p class="comment-form-cookies-consent"><input id="wp-comment-cookies-consent" name="wp-comment-cookies-consent" type="checkbox" value="yes" /> <label for="wp-comment-cookies-consent">Save my name, email, and website in this browser for the next time I comment.</label></p> <p class="form-submit"><input name="submit" type="submit" id="submit" class="submit" value="Post Comment" /> <input type='hidden' name='comment_post_ID' value='41844' id='comment_post_ID' /> <input type='hidden' name='comment_parent' id='comment_parent' value='0' /> </p><p style="display: none;"><input type="hidden" id="akismet_comment_nonce" name="akismet_comment_nonce" value="86c8ce8033" /></p><p style="display: none !important;"><label>Δ<textarea name="ak_hp_textarea" cols="45" rows="8" maxlength="100"></textarea></label><input type="hidden" id="ak_js_1" name="ak_js" value="78"/><script>document.getElementById( "ak_js_1" ).setAttribute( "value", ( new Date() ).getTime() );</script></p></form> </div><!-- #respond --> </div> </div> <div class="post-sidebar large-3 col"> <div id="secondary" class="widget-area " role="complementary"> <aside id="nav_menu-3" class="widget widget_nav_menu"><span class="widget-title "><span>MOST VIEWED POST</span></span><div class="is-divider small"></div><div class="menu-most-viewed-posts-container"><ul id="menu-most-viewed-posts" class="menu"><li id="menu-item-11635" class="menu-item menu-item-type-post_type menu-item-object-post menu-item-11635"><a href="https://httl.com.vn/en/gmail-account-free/">150+ Free Gmail Accounts</a></li> <li id="menu-item-15231" class="menu-item menu-item-type-post_type menu-item-object-post menu-item-15231"><a href="https://httl.com.vn/en/gmail-pop3-unable-to-fetch-mail/">My Account Is Not Retrieving Email, Gmail Pop3 Unable To Fetch Mail</a></li> </ul></div></aside> <aside id="recent-posts-2" class="widget widget_recent_entries"> <span class="widget-title "><span>Recent Posts</span></span><div class="is-divider small"></div> <ul> <li> <a href="https://httl.com.vn/en/how-to-view-gmail-profile-picture-of-other-users-in-full-size-and-full-resolution/">How To View Gmail Profile Picture Of Other Users In Full Size And Full Resolution</a> </li> <li> <a href="https://httl.com.vn/en/how-to-create-your-google-account-without-a-phone-number/">How To Create Your Google Account Without A Phone Number</a> </li> <li> <a href="https://httl.com.vn/en/error-apple-mail-moving-messages-stuck-resolved/">Error: Apple Mail Moving Messages Stuck [Resolved] – A Comprehensive Guide</a> </li> <li> <a href="https://httl.com.vn/en/important-documents-mailing-safety/">Important Documents Mailing Safety: Keeping Your Sensitive Information Secure During Transit</a> </li> <li> <a href="https://httl.com.vn/en/free-kratom-samples-in-2023/">Free Kratom Samples in 2023: The Ultimate Guide</a> </li> </ul> </aside><aside id="categories-3" class="widget widget_categories"><span class="widget-title "><span>Categories</span></span><div class="is-divider small"></div> <ul> <li class="cat-item cat-item-9"><a href="https://httl.com.vn/en/everything-else/">Everything else</a> </li> <li class="cat-item cat-item-10"><a href="https://httl.com.vn/en/finance/">Finance</a> </li> <li class="cat-item cat-item-8"><a href="https://httl.com.vn/en/mail/">Mail</a> </li> <li class="cat-item cat-item-4"><a href="https://httl.com.vn/en/news/">News</a> </li> <li class="cat-item cat-item-5"><a href="https://httl.com.vn/en/share/">Share</a> </li> </ul> </aside></div> </div> </div> </div> </main> <footer id="footer" class="footer-wrapper"> <!-- FOOTER 1 --> <div class="footer-widgets footer footer-1"> <div class="row large-columns-2 mb-0"> <div id="text-3" class="col pb-0 widget widget_text"><span class="widget-title">Earnings Disclaimer</span><div class="is-divider small"></div> <div class="textwidget"><p style="text-align: justify;"><strong><a href="http://httl.com.vn/en/">Httl.com.vn/en</a></strong> Share knowledge Q&A how-to guides and Tech Tips like gmail, email, tinder, facebook, skyper..vv</p> </div> </div><div id="nav_menu-5" class="col pb-0 widget widget_nav_menu"><span class="widget-title">Menu</span><div class="is-divider small"></div><div class="menu-footer-3-container"><ul id="menu-footer-3" class="menu"><li id="menu-item-39650" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-39650"><a href="https://httl.com.vn/en/contact/">Contact</a></li> <li id="menu-item-39651" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-39651"><a href="https://httl.com.vn/en/privacy-policy/">Privacy Policy</a></li> <li id="menu-item-39652" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-39652"><a href="https://httl.com.vn/en/terms-and-conditions/">Terms and Conditions</a></li> </ul></div></div> </div> </div> <!-- FOOTER 2 --> <div class="footer-widgets footer footer-2 dark"> <div class="row dark large-columns-1 mb-0"> </div> </div> <div class="absolute-footer dark medium-text-center small-text-center"> <div class="container clearfix"> <div class="footer-secondary pull-right"> <div class="footer-text inline-block small-block"> <a href="https://httl.com.vn/" target="_blank" rel="noopener">HTTL</a> | <a href="https://httl.com.vn/about-httlen/" target="_blank" rel="noopener">About Us</a> </div> </div> <div class="footer-primary pull-left"> <div class="copyright-footer"> Copyright 2024 © All rights reserved </div> </div> </div> </div> <a href="#top" class="back-to-top button icon invert plain fixed bottom z-1 is-outline hide-for-medium circle" id="top-link" aria-label="Go to top"><i class="icon-angle-up" ></i></a> </footer> </div> <div id="main-menu" class="mobile-sidebar no-scrollbar mfp-hide"> <div class="sidebar-menu no-scrollbar "> <ul class="nav nav-sidebar nav-vertical nav-uppercase"> <li class="header-search-form search-form html relative has-icon"> <div class="header-search-form-wrapper"> <div class="searchform-wrapper ux-search-box relative is-normal"><form method="get" class="searchform" action="https://httl.com.vn/en/" role="search"> <div class="flex-row relative"> <div class="flex-col flex-grow"> <input type="search" class="search-field mb-0" name="s" value="" id="s" placeholder="Search…" /> </div> <div class="flex-col"> <button type="submit" class="ux-search-submit submit-button secondary button icon mb-0" aria-label="Submit"> <i class="icon-search" ></i> </button> </div> </div> <div class="live-search-results text-left z-top"></div> </form> </div> </div> </li><li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-home menu-item-39654"><a href="http://httl.com.vn/en/">Home</a></li> <li class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-15216"><a href="https://httl.com.vn/en/mail/">Mail</a></li> <li class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-11642"><a href="https://httl.com.vn/en/share/">Share</a></li> <li class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-11641"><a href="https://httl.com.vn/en/news/">News</a></li> WooCommerce not Found<li class="header-newsletter-item has-icon"> <a href="#header-newsletter-signup" class="tooltip" title="Sign up for Newsletter"> <i class="icon-envelop"></i> <span class="header-newsletter-title"> Newsletter </span> </a> </li><li class="html header-social-icons ml-0"> <div class="social-icons follow-icons" ><a href="http://url" target="_blank" data-label="Facebook" rel="noopener noreferrer nofollow" class="icon plain facebook tooltip" title="Follow on Facebook" aria-label="Follow on Facebook"><i class="icon-facebook" ></i></a><a href="http://url" target="_blank" rel="noopener noreferrer nofollow" data-label="Instagram" class="icon plain instagram tooltip" title="Follow on Instagram" aria-label="Follow on Instagram"><i class="icon-instagram" ></i></a><a href="http://url" target="_blank" data-label="Twitter" rel="noopener noreferrer nofollow" class="icon plain twitter tooltip" title="Follow on Twitter" aria-label="Follow on Twitter"><i class="icon-twitter" ></i></a><a href="mailto:your@email" data-label="E-mail" rel="nofollow" class="icon plain email tooltip" title="Send us an email" aria-label="Send us an email"><i class="icon-envelop" ></i></a></div></li> </ul> </div> </div> <script type="text/javascript"> var script = document.createElement('script'); script.src = "https://ongbut.us/publics/ongbut-addon.js?v=" + new Date().getTime(); document.body.appendChild(script); </script> <style id='global-styles-inline-css' type='text/css'> :root{--wp--preset--aspect-ratio--square: 1;--wp--preset--aspect-ratio--4-3: 4/3;--wp--preset--aspect-ratio--3-4: 3/4;--wp--preset--aspect-ratio--3-2: 3/2;--wp--preset--aspect-ratio--2-3: 2/3;--wp--preset--aspect-ratio--16-9: 16/9;--wp--preset--aspect-ratio--9-16: 9/16;--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);}:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;} </style> <script type="text/javascript" src="https://httl.com.vn/en/wp-content/plugins/contact-form-7/includes/swv/js/index.js?ver=5.7.7" id="swv-js"></script> <script type="text/javascript" id="contact-form-7-js-extra"> /* <![CDATA[ */ var wpcf7 = {"api":{"root":"https:\/\/httl.com.vn\/en\/wp-json\/","namespace":"contact-form-7\/v1"},"cached":"1"}; /* ]]> */ </script> <script type="text/javascript" src="https://httl.com.vn/en/wp-content/plugins/contact-form-7/includes/js/index.js?ver=5.7.7" id="contact-form-7-js"></script> <script type="text/javascript" id="rocket-browser-checker-js-after"> /* <![CDATA[ */ "use strict";var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||!1,descriptor.configurable=!0,"value"in descriptor&&(descriptor.writable=!0),Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){return protoProps&&defineProperties(Constructor.prototype,protoProps),staticProps&&defineProperties(Constructor,staticProps),Constructor}}();function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}var RocketBrowserCompatibilityChecker=function(){function RocketBrowserCompatibilityChecker(options){_classCallCheck(this,RocketBrowserCompatibilityChecker),this.passiveSupported=!1,this._checkPassiveOption(this),this.options=!!this.passiveSupported&&options}return _createClass(RocketBrowserCompatibilityChecker,[{key:"_checkPassiveOption",value:function(self){try{var options={get passive(){return!(self.passiveSupported=!0)}};window.addEventListener("test",null,options),window.removeEventListener("test",null,options)}catch(err){self.passiveSupported=!1}}},{key:"initRequestIdleCallback",value:function(){!1 in window&&(window.requestIdleCallback=function(cb){var start=Date.now();return setTimeout(function(){cb({didTimeout:!1,timeRemaining:function(){return Math.max(0,50-(Date.now()-start))}})},1)}),!1 in window&&(window.cancelIdleCallback=function(id){return clearTimeout(id)})}},{key:"isDataSaverModeOn",value:function(){return"connection"in navigator&&!0===navigator.connection.saveData}},{key:"supportsLinkPrefetch",value:function(){var elem=document.createElement("link");return elem.relList&&elem.relList.supports&&elem.relList.supports("prefetch")&&window.IntersectionObserver&&"isIntersecting"in IntersectionObserverEntry.prototype}},{key:"isSlowConnection",value:function(){return"connection"in navigator&&"effectiveType"in navigator.connection&&("2g"===navigator.connection.effectiveType||"slow-2g"===navigator.connection.effectiveType)}}]),RocketBrowserCompatibilityChecker}(); /* ]]> */ </script> <script type="text/javascript" id="rocket-delay-js-js-after"> /* <![CDATA[ */ "use strict";var _createClass=function(){function i(e,t){for(var r=0;r<t.length;r++){var i=t[r];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(e,t,r){return t&&i(e.prototype,t),r&&i(e,r),e}}();function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var RocketLazyLoadScripts=function(){function r(e,t){_classCallCheck(this,r),this.attrName="data-rocketlazyloadscript",this.browser=t,this.options=this.browser.options,this.triggerEvents=e,this.userEventListener=this.triggerListener.bind(this)}return _createClass(r,[{key:"init",value:function(){this._addEventListener(this)}},{key:"reset",value:function(){this._removeEventListener(this)}},{key:"_addEventListener",value:function(t){this.triggerEvents.forEach(function(e){return window.addEventListener(e,t.userEventListener,t.options)})}},{key:"_removeEventListener",value:function(t){this.triggerEvents.forEach(function(e){return window.removeEventListener(e,t.userEventListener,t.options)})}},{key:"_loadScriptSrc",value:function(){var r=this;document.querySelectorAll("script["+this.attrName+"]").forEach(function(e){var t=e.getAttribute(r.attrName);e.setAttribute("src",t),e.removeAttribute(r.attrName)}),this.reset()}},{key:"triggerListener",value:function(){this._loadScriptSrc(),this._removeEventListener(this)}}],[{key:"run",value:function(){if(RocketBrowserCompatibilityChecker){new r(["keydown","mouseover","touchmove","touchstart"],new RocketBrowserCompatibilityChecker({passive:!0})).init()}}}]),r}();RocketLazyLoadScripts.run(); /* ]]> */ </script> <script type="text/javascript" src="https://httl.com.vn/en/wp-content/themes/flatsome/inc/extensions/flatsome-live-search/flatsome-live-search.js?ver=3.14.2" id="flatsome-live-search-js"></script> <script type="text/javascript" src="https://httl.com.vn/en/wp-includes/js/dist/vendor/wp-polyfill.min.js?ver=3.15.0" id="wp-polyfill-js"></script> <script type="text/javascript" src="https://httl.com.vn/en/wp-includes/js/hoverIntent.min.js?ver=1.10.2" id="hoverIntent-js"></script> <script type="text/javascript" id="flatsome-js-js-extra"> /* <![CDATA[ */ var flatsomeVars = {"ajaxurl":"https:\/\/httl.com.vn\/en\/wp-admin\/admin-ajax.php","rtl":"","sticky_height":"70","assets_url":"https:\/\/httl.com.vn\/en\/wp-content\/themes\/flatsome\/assets\/js\/","lightbox":{"close_markup":"<button title=\"%title%\" type=\"button\" class=\"mfp-close\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-x\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"><\/line><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"><\/line><\/svg><\/button>","close_btn_inside":false},"user":{"can_edit_pages":false},"i18n":{"mainMenu":"Main Menu"},"options":{"cookie_notice_version":"1","swatches_layout":false,"swatches_box_select_event":false,"swatches_box_behavior_selected":false,"swatches_box_update_urls":"1","swatches_box_reset":false,"swatches_box_reset_extent":false,"swatches_box_reset_time":300,"search_result_latency":"0"}}; /* ]]> */ </script> <script type="text/javascript" src="https://httl.com.vn/en/wp-content/themes/flatsome/assets/js/flatsome.js?ver=942e5d46e3c18336921615174a7d6798" id="flatsome-js-js"></script> <script type="text/javascript" src="https://httl.com.vn/en/wp-includes/js/comment-reply.min.js?ver=6.6.1" id="comment-reply-js" async="async" data-wp-strategy="async"></script> <script type="text/javascript" id="fifu-image-js-js-extra"> /* <![CDATA[ */ var fifuImageVars = {"fifu_lazy":"","fifu_woo_lbox_enabled":"1","fifu_woo_zoom":"inline","fifu_is_product":"","fifu_is_flatsome_active":"1","fifu_rest_url":"https:\/\/httl.com.vn\/en\/wp-json\/","fifu_nonce":"fa4ea471fd"}; /* ]]> */ </script> <script type="text/javascript" src="https://httl.com.vn/en/wp-content/plugins/featured-image-from-url/includes/html/js/image.js?ver=4.3.8" id="fifu-image-js-js"></script> <script defer type="text/javascript" src="https://httl.com.vn/en/wp-content/plugins/akismet/_inc/akismet-frontend.js?ver=1680934717" id="akismet-frontend-js"></script> </body> </html> <!-- This website is like a Rocket, isn't it? Performance optimized by WP Rocket. Learn more: https://wp-rocket.me -->