Adding opengraph tags. Fixes #5

This commit is contained in:
Dessalines 2020-09-11 13:09:21 -05:00
parent d909ec8096
commit 89d15c9d09
21 changed files with 246 additions and 165 deletions

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import {
UserOperation,
@ -25,6 +24,7 @@ import {
import autosize from 'autosize';
import { SiteForm } from './site-form';
import { UserListing } from './user-listing';
import { HtmlTags } from './html-tags';
import { i18n } from '../i18next';
interface AdminSettingsState {
@ -97,7 +97,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">

View file

@ -1,6 +1,7 @@
import { Component } from 'inferno';
import { Route, Switch } from 'inferno-router';
import { Provider } from 'inferno-i18next';
import { Helmet } from 'inferno-helmet';
import { i18n } from '../i18next';
import { routes } from '../../shared/routes';
import { Navbar } from '../../shared/components/navbar';
@ -23,6 +24,16 @@ export class App extends Component<AppProps, any> {
<>
<Provider i18next={i18n}>
<div>
{this.props.site.site.icon && (
<Helmet>
<link
id="favicon"
rel="icon"
type="image/x-icon"
href={this.props.site.site.icon}
/>
</Helmet>
)}
<Navbar site={this.props.site} />
<div class="mt-4 p-0 fl-1">
<Switch>

View file

@ -1,5 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { HtmlTags } from './html-tags';
import { Subscription } from 'rxjs';
import {
UserOperation,
@ -94,7 +94,10 @@ export class Communities extends Component<any, CommunitiesState> {
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
{this.state.loading ? (
<h5 class="">
<svg class="icon icon-spinner spin">

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { DataType } from '../interfaces';
import {
@ -27,6 +26,7 @@ import {
import { UserService, WebSocketService } from '../services';
import { PostListings } from './post-listings';
import { CommentNodes } from './comment-nodes';
import { HtmlTags } from './html-tags';
import { SortSelect } from './sort-select';
import { DataTypeSelect } from './data-type-select';
import { Sidebar } from './sidebar';
@ -46,7 +46,6 @@ import {
editPostFindRes,
commentsToFlatNodes,
setupTippy,
favIconUrl,
notifyPost,
setIsoData,
wsSubscribe,
@ -226,30 +225,12 @@ export class Community extends Component<any, State> {
}
get documentTitle(): string {
if (this.state.communityRes) {
return `${this.state.communityRes.community.title} - ${this.state.siteRes.site.name}`;
} else {
return 'Lemmy';
}
}
get favIcon(): string {
return this.state.siteRes.site.icon
? this.state.siteRes.site.icon
: favIconUrl;
return `${this.state.communityRes.community.title} - ${this.state.siteRes.site.name}`;
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle}>
<link
id="favicon"
rel="icon"
type="image/x-icon"
href={this.favIcon}
/>
</Helmet>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
@ -259,6 +240,12 @@ export class Community extends Component<any, State> {
) : (
<div class="row">
<div class="col-12 col-md-8">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
description={this.state.communityRes.community.title}
image={this.state.communityRes.community.icon}
/>
{this.communityInfo()}
{this.selects()}
{this.listings()}

View file

@ -1,7 +1,7 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { CommunityForm } from './community-form';
import { HtmlTags } from './html-tags';
import {
Community,
UserOperation,
@ -70,7 +70,10 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">

View file

@ -1,7 +1,7 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { PostForm } from './post-form';
import { HtmlTags } from './html-tags';
import {
isBrowser,
lemmyHttp,
@ -82,7 +82,10 @@ export class CreatePost extends Component<any, CreatePostState> {
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">

View file

@ -1,7 +1,7 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import { PrivateMessageForm } from './private-message-form';
import { HtmlTags } from './html-tags';
import { UserService, WebSocketService } from '../services';
import {
Site,
@ -102,7 +102,10 @@ export class CreatePrivateMessage extends Component<
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">

View file

@ -0,0 +1,52 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { httpExternalPath } from '../env';
interface HtmlTagsProps {
title: string;
path: string;
description?: string;
image?: string;
}
/// Taken from https://metatags.io/
export class HtmlTags extends Component<HtmlTagsProps, any> {
render() {
let url = httpExternalPath(this.props.path);
return (
<Helmet title={this.props.title}>
{/* Primary Meta Tags */}
<meta name="title" content={this.props.title} />
{this.props.description && (
<meta name="description" content={this.props.description} />
)}
{/* Open Graph / Facebook */}
<meta property="og:type" content="website" />
<meta property="og:url" content={url} />
<meta property="og:title" content={this.props.title} />
{this.props.description && (
<meta property="og:description" content={this.props.description} />
)}
{this.props.image && (
<meta property="og:image" content={this.props.image} />
)}
{/* Twitter */}
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={url} />
<meta property="twitter:title" content={this.props.title} />
{this.props.description && (
<meta
property="twitter:description"
content={this.props.description}
/>
)}
{this.props.image && (
<meta property="twitter:image" content={this.props.image} />
)}
</Helmet>
);
}
}

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import {
UserOperation,
@ -37,6 +36,7 @@ import {
} from '../utils';
import { CommentNodes } from './comment-nodes';
import { PrivateMessage } from './private-message';
import { HtmlTags } from './html-tags';
import { SortSelect } from './sort-select';
import { i18n } from '../i18next';
@ -117,7 +117,6 @@ export class Inbox extends Component<any, InboxState> {
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
@ -127,6 +126,10 @@ export class Inbox extends Component<any, InboxState> {
) : (
<div class="row">
<div class="col-12">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5 class="mb-1">
{i18n.t('inbox')}
<small>

View file

@ -1,8 +1,8 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { GetSiteResponse } from 'lemmy-js-client';
import { setIsoData } from '../utils';
import { i18n } from '../i18next';
import { HtmlTags } from './html-tags';
interface InstancesState {
siteRes: GetSiteResponse;
@ -26,7 +26,10 @@ export class Instances extends Component<any, InstancesState> {
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<div>
<h5>{i18n.t('linked_instances')}</h5>
{this.state.siteRes &&

View file

@ -22,6 +22,7 @@ import {
setIsoData,
} from '../utils';
import { i18n } from '../i18next';
import { HtmlTags } from './html-tags';
interface State {
loginForm: LoginForm;
@ -78,17 +79,16 @@ export class Login extends Component<any, State> {
}
get documentTitle(): string {
if (this.state.site.name) {
return `${i18n.t('login')} - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
return `${i18n.t('login')} - ${this.state.site.name}`;
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<div class="row">
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
<div class="col-12 col-lg-6">{this.registerForm()}</div>

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import {
@ -53,7 +52,6 @@ import {
editPostFindRes,
commentsToFlatNodes,
setupTippy,
favIconUrl,
notifyPost,
setIsoData,
wsSubscribe,
@ -63,6 +61,7 @@ import {
} from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
import { HtmlTags } from './html-tags';
interface MainState {
subscribedCommunities: CommunityUser[];
@ -240,23 +239,13 @@ export class Main extends Component<any, MainState> {
return `${this.state.siteRes.site.name}`;
}
get favIcon(): string {
return this.state.siteRes.site.icon
? this.state.siteRes.site.icon
: favIconUrl;
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle}>
<link
id="favicon"
rel="icon"
type="image/x-icon"
href={this.favIcon}
/>
</Helmet>
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<div class="row">
<main role="main" class="col-12 col-md-8">
{this.posts()}

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import {
@ -30,6 +29,7 @@ import {
lemmyHttp,
} from '../utils';
import { MomentTime } from './moment-time';
import { HtmlTags } from './html-tags';
import moment from 'moment';
import { i18n } from '../i18next';
@ -353,17 +353,16 @@ export class Modlog extends Component<any, ModlogState> {
}
get documentTitle(): string {
if (this.state.site) {
return `Modlog - ${this.state.site.name}`;
} else {
return 'Lemmy';
}
return `Modlog - ${this.state.site.name}`;
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
{this.state.loading ? (
<h5 class="">
<svg class="icon icon-spinner spin">

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import {
UserOperation,
@ -18,6 +17,7 @@ import {
wsSubscribe,
} from '../utils';
import { i18n } from '../i18next';
import { HtmlTags } from './html-tags';
interface State {
passwordChangeForm: PasswordChangeForm;
@ -61,7 +61,10 @@ export class PasswordChange extends Component<any, State> {
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('password_change')}</h5>

View file

@ -38,6 +38,7 @@ import {
previewLines,
} from '../utils';
import { i18n } from '../i18next';
import { externalHost } from '../env';
interface PostListingState {
showEdit: boolean;
@ -309,7 +310,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)}
</li>
<li className="list-inline-item"></li>
{post.url && !(hostname(post.url) == window.location.hostname) && (
{post.url && !(hostname(post.url) == externalHost) && (
<>
<li className="list-inline-item">
<a
@ -413,43 +414,37 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{post.name}
</Link>
)}
{(isImage(post.url) || this.props.post.thumbnail_url) && (
<>
{!this.state.imageExpanded ? (
{(isImage(post.url) || this.props.post.thumbnail_url) &&
(!this.state.imageExpanded ? (
<span
class="text-monospace unselectable pointer ml-2 text-muted small"
data-tippy-content={i18n.t('expand_here')}
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-plus-square"></use>
</svg>
</span>
) : (
<span>
<span
class="text-monospace unselectable pointer ml-2 text-muted small"
data-tippy-content={i18n.t('expand_here')}
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-plus-square"></use>
<use xlinkHref="#icon-minus-square"></use>
</svg>
</span>
) : (
<span>
<div>
<span
class="text-monospace unselectable pointer ml-2 text-muted small"
class="pointer"
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-minus-square"></use>
</svg>
<img class="img-fluid img-expanded" src={this.getImage()} />
</span>
<div>
<span
class="pointer"
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<img
class="img-fluid img-expanded"
src={this.getImage()}
/>
</span>
</div>
</span>
)}
</>
)}
</div>
</span>
))}
{post.removed && (
<small className="ml-2 text-muted font-italic">
{i18n.t('removed')}

View file

@ -1,5 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { HtmlTags } from './html-tags';
import { Subscription } from 'rxjs';
import {
UserOperation,
@ -36,7 +36,6 @@ import {
createPostLikeRes,
commentsToFlatNodes,
setupTippy,
favIconUrl,
setIsoData,
getIdFromProps,
getCommentIdFromProps,
@ -44,6 +43,8 @@ import {
setAuth,
lemmyHttp,
isBrowser,
previewLines,
isImage,
} from '../utils';
import { PostListing } from './post-listing';
import { Sidebar } from './sidebar';
@ -148,7 +149,7 @@ export class Post extends Component<any, PostState> {
// Necessary if you are on a post and you click another post (same route)
if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
// Couldnt get a refresh working. This does for now.
// TODO Couldnt get a refresh working. This does for now.
location.reload();
// let currentId = this.props.match.params.id;
@ -191,30 +192,29 @@ export class Post extends Component<any, PostState> {
}
get documentTitle(): string {
if (this.state.postRes) {
return `${this.state.postRes.post.name} - ${this.state.siteRes.site.name}`;
} else {
return 'Lemmy';
}
return `${this.state.postRes.post.name} - ${this.state.siteRes.site.name}`;
}
get favIcon(): string {
return this.state.siteRes.site.icon
? this.state.siteRes.site.icon
: favIconUrl;
get imageTag(): string {
return (
this.state.postRes.post.thumbnail_url ||
(this.state.postRes.post.url
? isImage(this.state.postRes.post.url)
? this.state.postRes.post.url
: undefined
: undefined)
);
}
get descriptionTag(): string {
return this.state.postRes.post.body
? previewLines(this.state.postRes.post.body)
: undefined;
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle}>
<link
id="favicon"
rel="icon"
type="image/x-icon"
href={this.favIcon}
/>
</Helmet>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
@ -224,6 +224,12 @@ export class Post extends Component<any, PostState> {
) : (
<div class="row">
<div class="col-12 col-md-8 mb-3">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
image={this.imageTag}
description={this.descriptionTag}
/>
<PostListing
post={this.state.postRes.post}
showBody

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Subscription } from 'rxjs';
import {
UserOperation,
@ -32,6 +31,7 @@ import {
setAuth,
} from '../utils';
import { PostListing } from './post-listing';
import { HtmlTags } from './html-tags';
import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
import { SortSelect } from './sort-select';
@ -165,23 +165,20 @@ export class Search extends Component<any, SearchState> {
}
get documentTitle(): string {
if (this.state.site.name) {
if (this.state.q) {
return `${i18n.t('search')} - ${this.state.q} - ${
this.state.site.name
}`;
} else {
return `${i18n.t('search')} - ${this.state.site.name}`;
}
if (this.state.q) {
return `${i18n.t('search')} - ${this.state.q} - ${this.state.site.name}`;
} else {
return 'Lemmy';
return `${i18n.t('search')} - ${this.state.site.name}`;
}
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
<h5>{i18n.t('search')}</h5>
{this.selects()}
{this.searchForm()}

View file

@ -1,10 +1,10 @@
import { Component } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Site } from 'lemmy-js-client';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
import { repoUrl, isBrowser } from '../utils';
import { IsoData } from 'shared/interfaces';
import { HtmlTags } from './html-tags';
interface SilverUser {
name: string;
@ -74,7 +74,10 @@ export class Sponsors extends Component<any, SponsorsState> {
render() {
return (
<div class="container text-center">
<Helmet title={this.documentTitle} />
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
{this.topMessage()}
<hr />
{this.sponsors()}

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import {
@ -33,7 +32,6 @@ import {
getLanguage,
mdToHtml,
elementUrl,
favIconUrl,
setIsoData,
getIdFromProps,
getUsernameFromProps,
@ -44,8 +42,10 @@ import {
createPostLikeFindRes,
setAuth,
lemmyHttp,
previewLines,
} from '../utils';
import { UserListing } from './user-listing';
import { HtmlTags } from './html-tags';
import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select';
import { MomentTime } from './moment-time';
@ -250,30 +250,18 @@ export class User extends Component<any, UserState> {
}
get documentTitle(): string {
if (this.state.siteRes.site.name) {
return `@${this.state.userName} - ${this.state.siteRes.site.name}`;
} else {
return 'Lemmy';
}
return `@${this.state.userName} - ${this.state.siteRes.site.name}`;
}
get favIcon(): string {
return this.state.siteRes.site.icon
? this.state.siteRes.site.icon
: favIconUrl;
get bioTag(): string {
return this.state.userRes.user.bio
? previewLines(this.state.userRes.user.bio)
: undefined;
}
render() {
return (
<div class="container">
<Helmet title={this.documentTitle}>
<link
id="favicon"
rel="icon"
type="image/x-icon"
href={this.favIcon}
/>
</Helmet>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
@ -284,6 +272,12 @@ export class User extends Component<any, UserState> {
<div class="row">
<div class="col-12 col-md-8">
<>
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
description={this.bioTag}
image={this.state.userRes.user.avatar}
/>
{this.userInfo()}
<hr />
</>

View file

@ -1,15 +1,46 @@
import { isBrowser } from './utils';
const nodeHostname = process.env.LEMMY_HOST || 'localhost'; // used for local dev
const host = isBrowser() ? window.location.hostname : nodeHostname;
const secure = isBrowser() && window.location.protocol == 'https:' ? 's' : '';
const port = isBrowser()
? window.location.port == '1234' || window.location.port == '1235'
? 8536
: window.location.port
: 8536;
const endpoint = `${host}:${port}`;
const testHost = 'localhost:8536';
export const wsUri = `ws${secure}://${endpoint}/api/v1/ws`;
export const httpUri = `http${secure}://${endpoint}/api/v1`;
export const pictrsUri = `http${secure}://${endpoint}/pictrs/image`;
const internalHost = process.env.LEMMY_INTERNAL_HOST || testHost; // used for local dev
export const externalHost = isBrowser()
? `${window.location.hostname}:${
window.location.port == '1234' || window.location.port == '1235'
? 8536
: window.location.port
}`
: process.env.LEMMY_EXTERNAL_HOST || testHost;
// ? window.location.port == '1234' || window.location.port == '1235'
const secure = isBrowser() && window.location.protocol == 'https:' ? 's' : '';
const host = isBrowser() ? externalHost : internalHost;
const httpBase = `http${secure}://${host}`;
export const wsUri = `ws${secure}://${host}/api/v1/ws`;
export const httpUri = `${httpBase}/api/v1`;
const httpExternalUri = `http${secure}://${externalHost}`;
export const pictrsUri = `${httpBase}/pictrs/image`;
console.log(`Internal host: ${internalHost}`);
console.log(`External host: ${externalHost}`);
export function httpExternalPath(path: string) {
return `${httpExternalUri}${path}`;
}
// export const httpUri = `http${secure}://${endpoint}/api/v1`;
// export const pictrsUri = `http${secure}://${endpoint}/pictrs/image`;
// const host = isBrowser() ? window.location.hostname : localHostname;
// const secure = isBrowser() && window.location.protocol == 'https:' ? 's' : '';
// const port = isBrowser()
// ? window.location.port == '1234' || window.location.port == '1235'
// ? 8536
// : window.location.port
// : 8536;
// const endpoint = `${host}:${port}`;
//
// export const wsUri = `ws${secure}://${endpoint}/api/v1/ws`;
// export const httpUri = `http${secure}://${endpoint}/api/v1`;
// export const pictrsUri = `http${secure}://${endpoint}/pictrs/image`;

View file

@ -271,7 +271,6 @@ export function isVideo(url: string) {
// TODO this broke
export function validURL(str: string) {
console.log(str);
// try {
return !!new URL(str);
// } catch {
@ -439,8 +438,6 @@ export function getMomentLanguage(): string {
export function setTheme(theme: string, forceReload: boolean = false) {
if (isBrowser() && (theme !== 'darkly' || forceReload)) {
console.log(`setting theme ${theme}`);
// Unload all the other themes
for (var i = 0; i < themes.length; i++) {
let styleSheet = document.getElementById(themes[i]);
@ -1078,11 +1075,7 @@ export function previewLines(
export function hostname(url: string): string {
let cUrl = new URL(url);
// TODO
return `${cUrl.hostname}:${cUrl.port}`;
// return window.location.port
// ? `${cUrl.hostname}:${cUrl.port}`
// : `${cUrl.hostname}`;
return cUrl.port ? `${cUrl.hostname}:${cUrl.port}` : `${cUrl.hostname}`;
}
function canUseWebP() {