Handle metadata in the database for a few reasons: the method is the same between traditional and vue, the user has control over the values, we can create dynamic titles a bit more easily, and with vue the values are populated before the SPA loads so search engines can pick it up more easily

This commit is contained in:
Kevin MacMartin 2024-04-03 22:35:43 -04:00
parent 338a2517b2
commit 2f5ed84e2b
19 changed files with 204 additions and 54 deletions

View File

@ -1,7 +1,6 @@
DEFAULT_LANGUAGE=en
APP_NAME='Hypothetical'
APP_DESC='A website template'
APP_ENV=local
APP_KEY=
APP_DEBUG=true

View File

@ -10,6 +10,12 @@ class Dashboard
* @return array
*/
public static $menu = [
[
'title' => 'Metadata',
'type' => 'edit',
'model' => 'meta'
],
[
'title' => 'Blog',
'type' => 'edit',

View File

@ -6,10 +6,16 @@
use App\Models\Blog;
use App\Models\Contact;
use App\Models\Subscriptions;
use App\Models\Meta;
use Illuminate\Http\Request;
class ApiController extends Controller {
public function getMeta($path = null)
{
return Meta::getData($path);
}
public function getBlogEntries()
{
return Blog::getBlogEntries();

54
app/Models/Meta.php Normal file
View File

@ -0,0 +1,54 @@
<?php
namespace App\Models;
class Meta extends DashboardModel
{
protected $table = 'meta';
public static $create = false;
public static $items_per_page = 0;
public static $dashboard_help_text = 'The path must start with a forward slash (eg: "/" or "/pagename")';
public static $dashboard_type = 'edit';
public static $dashboard_display = [ 'title', 'path' ];
public static $dashboard_columns = [
[ 'name' => 'path', 'required' => true, 'unique' => true, 'type' => 'string' ],
[ 'name' => 'title', 'required' => true, 'unique' => false, 'type' => 'string' ],
[ 'name' => 'description', 'required' => true, 'unique' => false, 'type' => 'text' ],
[ 'name' => 'keywords', 'required' => true, 'unique' => false, 'type' => 'string' ]
];
public static function getData($path)
{
if (!preg_match('/^\//', $path)) {
$path = "/$path";
}
if (preg_match('/^\/(dashboard|login|register)/', $path)) {
$page = [
'title' => 'Dashboard' . ' | ' . env('APP_NAME'),
'description' => '',
'keywords' => ''
];
} else {
$page = self::select('title', 'description', 'keywords')->where('path', "$path")->first();
if ($page == null) {
$page = [
'title' => 'Page Not Found' . ' | ' . env('APP_NAME'),
'description' => 'The requested page cannot be found',
'keywords' => ''
];
} else {
$page['title'] = $page['title'] . ' | ' . env('APP_NAME');
}
}
return $page;
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('meta', function (Blueprint $table) {
$table->id();
$table->string('path')->nullable();
$table->text('title')->nullable();
$table->text('description')->nullable();
$table->text('keywords')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('meta');
}
};

View File

@ -13,6 +13,6 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
$this->call(MetaSeeder::class);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Database\Seeders;
use DB;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Meta;
class MetaSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Delete the table
DB::table('meta')->delete();
// Page metadata
$pages = [
[
'path' => '/',
'title' => 'Home',
'description' => '',
'keywords' => ''
],
[
'path' => '/blog',
'title' => 'Blog',
'description' => '',
'keywords' => ''
],
[
'path' => '/contact',
'title' => 'Contact',
'description' => '',
'keywords' => ''
]
];
foreach ($pages as $page) {
Meta::create([
'path' => $page['path'],
'title' => $page['title'],
'description' => $page['description'],
'keywords' => $page['keywords']
]);
}
}
}

View File

@ -69,9 +69,6 @@
data() {
return {
metaTitle: "Contact",
metaDescription: "Contact Us",
metaKeywords: "contact",
submitting: false,
errorCount: 0,
submitSuccess: false,

View File

@ -10,13 +10,6 @@
export default {
mixins: [
BasePageMixin
],
data() {
return {
metaTitle: "Page Not Found",
metaDescription: "The requested page cannot be found"
};
}
]
};
</script>

View File

@ -15,12 +15,6 @@
components: {
"subscription-form": SubscriptionFormSection
},
data() {
return {
metaKeywords: "home"
};
}
};
</script>

View File

@ -75,6 +75,7 @@ const store = createStore({
appLang: env.appLang,
appDefaultLang: env.appDefaultLang,
firstLoad: true,
firstPage: true,
lastPath: "",
supportsWebP: null
},
@ -96,6 +97,10 @@ const store = createStore({
return state.firstLoad;
},
getFirstPage: state => {
return state.firstPage;
},
getLastPath: state => {
return state.lastPath;
},
@ -115,6 +120,10 @@ const store = createStore({
state.firstLoad = value;
},
setFirstPage(state, value) {
state.firstPage = value;
},
setLastPath(state, value) {
state.lastPath = value;
},

View File

@ -1,10 +1,6 @@
export default {
data() {
return {
metaTitle: "",
metaDescription: "",
metaKeywords: "",
metaTags: {
"title": [ "name", "title" ],
"description": [ "name", "description" ],
@ -21,14 +17,6 @@ export default {
},
computed: {
pageTitle() {
return this.metaTitle === "" ? env.appName : `${this.metaTitle} | ${env.appName}`;
},
pageDescription() {
return this.metaDescription === "" ? env.appDesc : this.metaDescription;
},
fullPath() {
return document.location.origin + this.$route.path;
}
@ -43,22 +31,22 @@ export default {
}
},
updateMetaData() {
updateMetadata(meta) {
let metaContent;
document.title = this.pageTitle;
document.title = meta.title;
$("link[rel=canonical]").attr("href", this.fullPath);
Object.keys(this.metaTags).forEach((name) => {
switch (this.metaTags[name][1]) {
case "title":
metaContent = this.pageTitle;
metaContent = meta.title;
break;
case "description":
metaContent = this.pageDescription;
metaContent = meta.description;
break;
case "keywords":
metaContent = this.metaKeywords;
metaContent = meta.keywords;
break;
case "url":
metaContent = this.fullPath;
@ -69,10 +57,24 @@ export default {
this.updateMetaTag(this.metaTags[name][0], name, metaContent);
});
},
fetchMetadata() {
this.$http.get(`/api/meta${this.$route.path}${env.apiToken}`).then((response) => {
this.updateMetadata(response.data);
}).catch((error) => {
console.log("error fetching metadata");
this.updateMetadata({ title: appName, description: "", keywords: "" });
});
}
},
created() {
this.updateMetaData();
// Don't fetch metadata on the first page load as this is handled by the page render
if (this.$store.getters.getFirstPage) {
this.$store.commit("setFirstPage", false);
} else {
this.fetchMetadata();
}
}
};

View File

@ -1,3 +1 @@
@extends('templates.error', [
'title' => 'Page Not Found'
])
@extends('templates.error')

View File

@ -1,8 +1,15 @@
<!DOCTYPE html>
<html lang="{{ Language::getSessionLanguage() }}">
@php
$page_title = (isset($title) ? $title . ' - ' : '') . env('APP_NAME');
// Determine whether the device is mobile
$device_mobile = !is_null(Request::header('User-Agent')) && (preg_match('/Mobi/', Request::header('User-Agent')) || preg_match('/iP(hone|ad|od);/', Request::header('User-Agent')));
// If an overridden title has been set (error pages) then use that, otherwise use the Meta model to populate metadata
if (isset($title)) {
$meta = [ 'title' => $title . ' - ' . env('APP_NAME'), 'description' => '', 'keywords' => '' ];
} else {
$meta = App\Models\Meta::getData(Request::path());
}
@endphp
<head>
@ -11,22 +18,23 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#fcfcfc" />
<title>{{ $page_title }}</title>
<title>{{ $meta['title'] }}</title>
<meta name="title" content="{{ $page_title }}" />
<meta name="description" content="{{ env('APP_DESC') }}" />
<meta name="dc:title" content="{{ $page_title }}" />
<meta name="dc:description" content="{{ env('APP_DESC') }}" />
<meta name="title" content="{{ $meta['title'] }}" />
<meta name="description" content="{{ $meta['description'] }}" />
<meta name="keywords" content="{{ $meta['keywords'] }}" />
<meta name="dc:title" content="{{ $meta['title'] }}" />
<meta name="dc:description" content="{{ $meta['description'] }}" />
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ $page_title }}" />
<meta property="og:description" content="{{ env('APP_DESC') }}" />
<meta property="og:title" content="{{ $meta['title'] }}" />
<meta property="og:description" content="{{ $meta['description'] }}" />
<meta property="og:url" content="{{ Request::url() }}" />
<meta property="og:image" content="{{ asset('/img/logo.png') }}" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ $page_title }}" />
<meta name="twitter:description" content="{{ env('APP_DESC') }}" />
<meta name="twitter:title" content="{{ $meta['title'] }}" />
<meta name="twitter:description" content="{{ $meta['description'] }}" />
<meta name="twitter:image" content="{{ asset('/img/logo.png') }}" />
<link rel="shortcut icon" href="{{ URL::to('/') }}/favicon.ico?version={{ Version::get() }}" />

View File

@ -1,4 +1,4 @@
@extends('templates.base', [ 'title' => 'Dashboard' ])
@extends('templates.base')
@php
$current_page = preg_match('/\/settings$/', Request::url()) ? 'settings' : preg_replace([ '/https?:\/\/[^\/]*\/dashboard\/[^\/]*\//', '/\/.*/' ], [ '', '' ], Request::url());

View File

@ -7,7 +7,6 @@
<script>
var env = {
appName: "{!! env('APP_NAME') !!}",
appDesc: "{!! env('APP_DESC') !!}",
appLang: "{{ Language::getSessionLanguage() }}",
appDefaultLang: "{{ env('DEFAULT_LANGUAGE') }}",
apiToken: "{{ Auth::check() ? '?api_token=' . Auth::user()->api_token : '' }}",

View File

@ -10,6 +10,7 @@
*/
Route::get('/blog-entries', 'App\Http\Controllers\ApiController@getBlogEntries');
Route::get('/meta/{path?}', 'App\Http\Controllers\ApiController@getMeta');
/*
|--------------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
@extends('templates.public', [ 'title' => 'Blog' ])
@extends('templates.public')
@section('content')
<div class="blog-page-component">

View File

@ -1,4 +1,4 @@
@extends('templates.public', [ 'title' => 'Contact' ])
@extends('templates.public')
@section('content')
<div class="contact-page-component">