Update (seo)
Add (lqip)
This commit is contained in:
@@ -5,12 +5,22 @@
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Post;
|
use App\Models\Post;
|
||||||
|
use Artesaos\SEOTools\Facades\SEOTools;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class FrontListController extends Controller
|
class FrontListController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
$title = 'Latest News from EchoScoop';
|
||||||
|
|
||||||
|
SEOTools::metatags();
|
||||||
|
SEOTools::twitter();
|
||||||
|
SEOTools::opengraph();
|
||||||
|
SEOTools::jsonLd();
|
||||||
|
SEOTools::setTitle($title, false);
|
||||||
|
|
||||||
$posts = Post::where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
$posts = Post::where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
||||||
|
|
||||||
return view('front.post_list', compact('posts'));
|
return view('front.post_list', compact('posts'));
|
||||||
@@ -22,6 +32,14 @@ public function category(Request $request, $category_slug)
|
|||||||
|
|
||||||
$posts = $category?->posts()->where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
$posts = $category?->posts()->where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
||||||
|
|
||||||
|
$title = $category->name.' News from EchoScoop';
|
||||||
|
|
||||||
|
SEOTools::metatags();
|
||||||
|
SEOTools::twitter();
|
||||||
|
SEOTools::opengraph();
|
||||||
|
SEOTools::jsonLd();
|
||||||
|
SEOTools::setTitle($title, false);
|
||||||
|
|
||||||
return view('front.post_list', compact('category', 'posts'));
|
return view('front.post_list', compact('category', 'posts'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Post;
|
use App\Models\Post;
|
||||||
|
use Artesaos\SEOTools\Facades\JsonLdMulti;
|
||||||
|
use Artesaos\SEOTools\Facades\OpenGraph;
|
||||||
|
use Artesaos\SEOTools\Facades\SEOMeta;
|
||||||
use GrahamCampbell\Markdown\Facades\Markdown;
|
use GrahamCampbell\Markdown\Facades\Markdown;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Symfony\Component\DomCrawler\Crawler;
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
@@ -25,6 +28,46 @@ public function index(Request $request, $slug)
|
|||||||
$content = $this->injectTableOfContents($content);
|
$content = $this->injectTableOfContents($content);
|
||||||
$content = $this->injectFeaturedImage($post, $content);
|
$content = $this->injectFeaturedImage($post, $content);
|
||||||
|
|
||||||
|
$post_description = $post->excerpt.' '.$post->title.' by EchoScoop.';
|
||||||
|
|
||||||
|
SEOMeta::setTitle($post->title, false);
|
||||||
|
SEOMeta::setDescription($post_description);
|
||||||
|
SEOMeta::addMeta('article:published_time', $post->published_at->format('Y-m-d'), 'property');
|
||||||
|
SEOMeta::addMeta('article:section', $post->category->name, 'property');
|
||||||
|
SEOMeta::setRobots('INDEX, FOLLOW, MAX-IMAGE-PREVIEW:LARGE, MAX-SNIPPET:-1, MAX-VIDEO-PREVIEW:-1');
|
||||||
|
|
||||||
|
OpenGraph::setDescription($post_description);
|
||||||
|
OpenGraph::setTitle($post->title);
|
||||||
|
OpenGraph::setUrl(url()->current());
|
||||||
|
OpenGraph::addProperty('type', 'article');
|
||||||
|
OpenGraph::addProperty('locale', 'en_US');
|
||||||
|
OpenGraph::addImage($post->featured_image_cdn);
|
||||||
|
|
||||||
|
$jsonld_multi = JsonLdMulti::newJsonLd();
|
||||||
|
$jsonld_multi->setTitle($post->title)
|
||||||
|
->setDescription($post_description)
|
||||||
|
->setType('Article')
|
||||||
|
->addValue('headline', $post->title)
|
||||||
|
->addImage($post->featured_image_cdn)
|
||||||
|
->addValue('author', [
|
||||||
|
'type' => 'Person',
|
||||||
|
'name' => $post->author->name,
|
||||||
|
'url' => config('app.url'),
|
||||||
|
])
|
||||||
|
->addValue('publisher', [
|
||||||
|
'type' => 'Organization',
|
||||||
|
'name' => 'EchoScoop',
|
||||||
|
'logo' => [
|
||||||
|
'type' => 'ImageObject',
|
||||||
|
'url' => asset('echoscoop-logo-512x512.png'),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->addValue('datePublished', $post->published_at->format('Y-m-d'))
|
||||||
|
->addValue('dateCreated', $post->created_at->format('Y-m-d'))
|
||||||
|
->addValue('dateModified', $post->updated_at->format('Y-m-d'))
|
||||||
|
->addValue('description', $post_description)
|
||||||
|
->addValue('articleBody', trim(preg_replace('/\s\s+/', ' ', strip_tags($content))));
|
||||||
|
|
||||||
return view('front.single_post', compact('post', 'content'));
|
return view('front.single_post', compact('post', 'content'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +75,7 @@ private function injectBootstrapClasses($content)
|
|||||||
{
|
{
|
||||||
$crawler = new Crawler($content);
|
$crawler = new Crawler($content);
|
||||||
|
|
||||||
|
// Handle Headings
|
||||||
$crawler->filter('h1')->each(function (Crawler $node) {
|
$crawler->filter('h1')->each(function (Crawler $node) {
|
||||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' display-6 fw-bolder mt-3 mb-4'));
|
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' display-6 fw-bolder mt-3 mb-4'));
|
||||||
});
|
});
|
||||||
@@ -41,13 +85,13 @@ private function injectBootstrapClasses($content)
|
|||||||
});
|
});
|
||||||
|
|
||||||
$crawler->filter('h3')->each(function (Crawler $node) {
|
$crawler->filter('h3')->each(function (Crawler $node) {
|
||||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'h6 mb-2'));
|
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' h6 mb-3'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Paragraphs
|
||||||
$crawler->filter('p')->each(function (Crawler $pNode) {
|
$crawler->filter('p')->each(function (Crawler $pNode) {
|
||||||
$precedingHeaders = $pNode->previousAll()->filter('h2');
|
$precedingHeaders = $pNode->previousAll()->filter('h2');
|
||||||
|
|
||||||
// If there are no preceding <h2> tags, just process the <p>
|
|
||||||
if (! $precedingHeaders->count()) {
|
if (! $precedingHeaders->count()) {
|
||||||
$existingClasses = $pNode->attr('class');
|
$existingClasses = $pNode->attr('class');
|
||||||
$newClasses = trim($existingClasses.' mt-2 mb-4');
|
$newClasses = trim($existingClasses.' mt-2 mb-4');
|
||||||
@@ -70,6 +114,7 @@ private function injectBootstrapClasses($content)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Lists
|
||||||
$crawler->filter('ul')->each(function (Crawler $node) {
|
$crawler->filter('ul')->each(function (Crawler $node) {
|
||||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' py-2'));
|
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' py-2'));
|
||||||
});
|
});
|
||||||
@@ -78,6 +123,11 @@ private function injectBootstrapClasses($content)
|
|||||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' py-2'));
|
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' py-2'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Tables
|
||||||
|
$crawler->filter('table')->each(function (Crawler $node) {
|
||||||
|
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' table table-bordered mb-4'));
|
||||||
|
});
|
||||||
|
|
||||||
// Convert the modified DOM back to string
|
// Convert the modified DOM back to string
|
||||||
$modifiedContent = '';
|
$modifiedContent = '';
|
||||||
foreach ($crawler as $domElement) {
|
foreach ($crawler as $domElement) {
|
||||||
@@ -117,16 +167,16 @@ private function injectTableOfContents($html)
|
|||||||
|
|
||||||
private function injectFeaturedImage($post, $content)
|
private function injectFeaturedImage($post, $content)
|
||||||
{
|
{
|
||||||
if (! is_empty($post->featured_image)) {
|
if (! empty($post->featured_image)) {
|
||||||
$featured_image_alt = strtolower($post->short_title);
|
$featured_image_alt = strtolower($post->short_title);
|
||||||
$featured_image_alt_caps = strtoupper($post->short_title);
|
$featured_image_alt_caps = strtoupper($post->short_title);
|
||||||
$featured_image = "<div class=\"w-100 d-flex justify-content-center\"><figure class=\"text-center\"><img decoding=\"async\" class=\"img-fluid rounded mb-2 shadow\" src=\"{$post->featured_image_cdn}\" alt=\"{$featured_image_alt}\"><figcaption class=\"text-secondary small\">{$featured_image_alt_caps}</figcaption></figure></div>";
|
|
||||||
|
// Update the HTML structure
|
||||||
|
$featured_image = "<div class=\"w-100 d-flex justify-content-center\"><figure class=\"text-center\"><img decoding=\"async\" class=\"img-fluid rounded mb-2 shadow lqip-blur\" src=\"{$post->featured_image_lqip_cdn}\" data-src=\"{$post->featured_image_cdn}\" alt=\"{$featured_image_alt}\"><figcaption class=\"text-secondary small\">{$featured_image_alt_caps}</figcaption></figure></div>";
|
||||||
|
|
||||||
$content = preg_replace('/(<\/h1>)/', '$1'.$featured_image, $content, 1);
|
$content = preg_replace('/(<\/h1>)/', '$1'.$featured_image, $content, 1);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $content;
|
return $content;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,7 +208,9 @@ public static function handle($post)
|
|||||||
$yPosition += $fontSize + $padding;
|
$yPosition += $fontSize + $padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = $post->slug.'-'.epoch_now_timestamp().'.jpg';
|
$epoch_now_timestamp = epoch_now_timestamp();
|
||||||
|
|
||||||
|
$filename = $post->slug.'-'.$epoch_now_timestamp.'.jpg';
|
||||||
|
|
||||||
$ok = OSSUploader::uploadFile('r2', 'post_images/', $filename, (string) $canvas->stream('jpeg'));
|
$ok = OSSUploader::uploadFile('r2', 'post_images/', $filename, (string) $canvas->stream('jpeg'));
|
||||||
|
|
||||||
@@ -216,18 +218,20 @@ public static function handle($post)
|
|||||||
// Clone the main image for LQIP version
|
// Clone the main image for LQIP version
|
||||||
$lqipImage = clone $canvas;
|
$lqipImage = clone $canvas;
|
||||||
|
|
||||||
// Create the LQIP version of the image
|
// Apply blur
|
||||||
$lqipImage->fit(10, 10, function ($constraint) {
|
$lqipImage->blur(5); // Adjust to the desired blur level
|
||||||
$constraint->aspectRatio();
|
|
||||||
});
|
|
||||||
|
|
||||||
$lqipImage->encode('jpg', 5);
|
// Pixelate the image
|
||||||
|
$lqipImage->pixelate(30); // Adjust this value based on the desired pixelation level
|
||||||
|
|
||||||
|
// Encode the image to a low-quality JPG
|
||||||
|
$lqipImage->encode('webp', 5);
|
||||||
|
|
||||||
// LQIP filename
|
// LQIP filename
|
||||||
$lqip_filename = $post->slug.'-'.epoch_now_timestamp().'_lqip.jpg';
|
$lqip_filename = $post->slug.'-'.$epoch_now_timestamp.'_lqip.webp';
|
||||||
|
|
||||||
// Upload the LQIP version using OSSUploader
|
// Upload the LQIP version using OSSUploader
|
||||||
$lqip_ok = OSSUploader::uploadFile('r2', 'post_images/', $lqip_filename, (string) $lqipImage->stream('jpeg'));
|
$lqip_ok = OSSUploader::uploadFile('r2', 'post_images/', $lqip_filename, (string) $lqipImage->stream('webp'));
|
||||||
|
|
||||||
if ($ok && $lqip_ok) {
|
if ($ok && $lqip_ok) {
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ public function getFeaturedImageLqipCdnAttribute()
|
|||||||
$extension = pathinfo($this->featured_image, PATHINFO_EXTENSION);
|
$extension = pathinfo($this->featured_image, PATHINFO_EXTENSION);
|
||||||
|
|
||||||
// Append "_lqip" before the extension to create the LQIP image URL
|
// Append "_lqip" before the extension to create the LQIP image URL
|
||||||
$lqipFeaturedImage = str_replace(".{$extension}", "_lqip.{$extension}", $this->featured_image);
|
$lqipFeaturedImage = str_replace(".{$extension}", '_lqip.webp', $this->featured_image);
|
||||||
|
|
||||||
return 'https://'.Storage::disk('r2')->url($lqipFeaturedImage);
|
return 'https://'.Storage::disk('r2')->url($lqipFeaturedImage).'?a=bc';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
'description' => 'Distilling world news into bite-sized scoops.', // set false to total remove
|
'description' => 'Distilling world news into bite-sized scoops.', // set false to total remove
|
||||||
'separator' => ' - ',
|
'separator' => ' - ',
|
||||||
'keywords' => [],
|
'keywords' => [],
|
||||||
'canonical' => false, // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
|
'canonical' => 'current', // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
|
||||||
'robots' => false, // Set to 'all', 'none' or any combination of index/noindex and follow/nofollow
|
'robots' => false, // Set to 'all', 'none' or any combination of index/noindex and follow/nofollow
|
||||||
],
|
],
|
||||||
/*
|
/*
|
||||||
|
|||||||
1
public/build/assets/LqipLoader-2067e882.js
Normal file
1
public/build/assets/LqipLoader-2067e882.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{_ as i}from"./vue-8b92bff6.js";const n={name:"LqipLoader",mounted(){this.initLqipLoading()},methods:{initLqipLoading(){const e=document.getElementsByTagName("img");for(let t=0;t<e.length;t++)e[t].getAttribute("data-src")&&(e[t].onload=function(){this.classList.remove("lqip-blur")},e[t].setAttribute("src",e[t].getAttribute("data-src")))}}};function o(e,t,r,s,a,c){return null}const m=i(n,[["render",o]]);export{m as default};
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
5
public/build/assets/app-front-c970ee86.js
Normal file
5
public/build/assets/app-front-c970ee86.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/app-front-c970ee86.js.gz
Normal file
BIN
public/build/assets/app-front-c970ee86.js.gz
Normal file
Binary file not shown.
9
public/build/assets/app-front-f3aa4148.css
Normal file
9
public/build/assets/app-front-f3aa4148.css
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/app-front-f3aa4148.css.gz
Normal file
BIN
public/build/assets/app-front-f3aa4148.css.gz
Normal file
Binary file not shown.
@@ -58,20 +58,31 @@
|
|||||||
"src": "resources/js/app-auth.js"
|
"src": "resources/js/app-auth.js"
|
||||||
},
|
},
|
||||||
"resources/js/app-front.js": {
|
"resources/js/app-front.js": {
|
||||||
"file": "assets/app-front-83914abf.js",
|
"dynamicImports": [
|
||||||
|
"resources/js/vue/front/LqipLoader.vue"
|
||||||
|
],
|
||||||
|
"file": "assets/app-front-c970ee86.js",
|
||||||
"imports": [
|
"imports": [
|
||||||
"_vue-8b92bff6.js"
|
"_vue-8b92bff6.js"
|
||||||
],
|
],
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "resources/js/app-front.js"
|
"src": "resources/js/app-front.js"
|
||||||
},
|
},
|
||||||
|
"resources/js/vue/front/LqipLoader.vue": {
|
||||||
|
"file": "assets/LqipLoader-2067e882.js",
|
||||||
|
"imports": [
|
||||||
|
"_vue-8b92bff6.js"
|
||||||
|
],
|
||||||
|
"isDynamicEntry": true,
|
||||||
|
"src": "resources/js/vue/front/LqipLoader.vue"
|
||||||
|
},
|
||||||
"resources/sass/app-auth.scss": {
|
"resources/sass/app-auth.scss": {
|
||||||
"file": "assets/app-auth-d32494d2.css",
|
"file": "assets/app-auth-d32494d2.css",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "resources/sass/app-auth.scss"
|
"src": "resources/sass/app-auth.scss"
|
||||||
},
|
},
|
||||||
"resources/sass/app-front.scss": {
|
"resources/sass/app-front.scss": {
|
||||||
"file": "assets/app-front-48c01c04.css",
|
"file": "assets/app-front-f3aa4148.css",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "resources/sass/app-front.scss"
|
"src": "resources/sass/app-front.scss"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
@@ -19,3 +19,19 @@ .nav-scroller .nav {
|
|||||||
.hover-text-decoration-underline:hover {
|
.hover-text-decoration-underline:hover {
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* lqip start */
|
||||||
|
|
||||||
|
img {
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
img.lqip-blur {
|
||||||
|
opacity: 1; /* or any value less than 1 to make it slightly transparent */
|
||||||
|
}
|
||||||
|
|
||||||
|
img.lqip-blur {
|
||||||
|
filter: blur(20px);
|
||||||
|
-webkit-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* lqip end */
|
||||||
|
|||||||
25
resources/js/vue/front/LqipLoader.vue
Normal file
25
resources/js/vue/front/LqipLoader.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Empty template -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "LqipLoader",
|
||||||
|
mounted() {
|
||||||
|
this.initLqipLoading();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initLqipLoading() {
|
||||||
|
const imgDefer = document.getElementsByTagName("img");
|
||||||
|
for (let i = 0; i < imgDefer.length; i++) {
|
||||||
|
if (imgDefer[i].getAttribute("data-src")) {
|
||||||
|
imgDefer[i].onload = function () {
|
||||||
|
this.classList.remove("lqip-blur");
|
||||||
|
};
|
||||||
|
imgDefer[i].setAttribute("src", imgDefer[i].getAttribute("data-src"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
@yield('content')
|
@yield('content')
|
||||||
</main>
|
</main>
|
||||||
@include('front.layouts.partials.footer')
|
@include('front.layouts.partials.footer')
|
||||||
|
<lqip-loader></lqip-loader>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
@if ($enabled)
|
@if ($enabled)
|
||||||
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user