mirror of
https://github.com/prurigro/hypothetical.git
synced 2024-11-23 08:24:10 -05:00
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:
parent
338a2517b2
commit
2f5ed84e2b
19 changed files with 204 additions and 54 deletions
|
@ -1,7 +1,6 @@
|
||||||
DEFAULT_LANGUAGE=en
|
DEFAULT_LANGUAGE=en
|
||||||
|
|
||||||
APP_NAME='Hypothetical'
|
APP_NAME='Hypothetical'
|
||||||
APP_DESC='A website template'
|
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
|
|
|
@ -10,6 +10,12 @@ class Dashboard
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static $menu = [
|
public static $menu = [
|
||||||
|
[
|
||||||
|
'title' => 'Metadata',
|
||||||
|
'type' => 'edit',
|
||||||
|
'model' => 'meta'
|
||||||
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
'title' => 'Blog',
|
'title' => 'Blog',
|
||||||
'type' => 'edit',
|
'type' => 'edit',
|
||||||
|
|
|
@ -6,10 +6,16 @@ use Newsletter;
|
||||||
use App\Models\Blog;
|
use App\Models\Blog;
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
use App\Models\Subscriptions;
|
use App\Models\Subscriptions;
|
||||||
|
use App\Models\Meta;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ApiController extends Controller {
|
class ApiController extends Controller {
|
||||||
|
|
||||||
|
public function getMeta($path = null)
|
||||||
|
{
|
||||||
|
return Meta::getData($path);
|
||||||
|
}
|
||||||
|
|
||||||
public function getBlogEntries()
|
public function getBlogEntries()
|
||||||
{
|
{
|
||||||
return Blog::getBlogEntries();
|
return Blog::getBlogEntries();
|
||||||
|
|
54
app/Models/Meta.php
Normal file
54
app/Models/Meta.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
31
database/migrations/2024_04_03_164823_create_meta_table.php
Normal file
31
database/migrations/2024_04_03_164823_create_meta_table.php
Normal 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');
|
||||||
|
}
|
||||||
|
};
|
|
@ -13,6 +13,6 @@ class DatabaseSeeder extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
|
$this->call(MetaSeeder::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
53
database/seeders/MetaSeeder.php
Normal file
53
database/seeders/MetaSeeder.php
Normal 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']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,9 +69,6 @@
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
metaTitle: "Contact",
|
|
||||||
metaDescription: "Contact Us",
|
|
||||||
metaKeywords: "contact",
|
|
||||||
submitting: false,
|
submitting: false,
|
||||||
errorCount: 0,
|
errorCount: 0,
|
||||||
submitSuccess: false,
|
submitSuccess: false,
|
||||||
|
|
|
@ -10,13 +10,6 @@
|
||||||
export default {
|
export default {
|
||||||
mixins: [
|
mixins: [
|
||||||
BasePageMixin
|
BasePageMixin
|
||||||
],
|
]
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
metaTitle: "Page Not Found",
|
|
||||||
metaDescription: "The requested page cannot be found"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -15,12 +15,6 @@
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
"subscription-form": SubscriptionFormSection
|
"subscription-form": SubscriptionFormSection
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
metaKeywords: "home"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -75,6 +75,7 @@ const store = createStore({
|
||||||
appLang: env.appLang,
|
appLang: env.appLang,
|
||||||
appDefaultLang: env.appDefaultLang,
|
appDefaultLang: env.appDefaultLang,
|
||||||
firstLoad: true,
|
firstLoad: true,
|
||||||
|
firstPage: true,
|
||||||
lastPath: "",
|
lastPath: "",
|
||||||
supportsWebP: null
|
supportsWebP: null
|
||||||
},
|
},
|
||||||
|
@ -96,6 +97,10 @@ const store = createStore({
|
||||||
return state.firstLoad;
|
return state.firstLoad;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFirstPage: state => {
|
||||||
|
return state.firstPage;
|
||||||
|
},
|
||||||
|
|
||||||
getLastPath: state => {
|
getLastPath: state => {
|
||||||
return state.lastPath;
|
return state.lastPath;
|
||||||
},
|
},
|
||||||
|
@ -115,6 +120,10 @@ const store = createStore({
|
||||||
state.firstLoad = value;
|
state.firstLoad = value;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFirstPage(state, value) {
|
||||||
|
state.firstPage = value;
|
||||||
|
},
|
||||||
|
|
||||||
setLastPath(state, value) {
|
setLastPath(state, value) {
|
||||||
state.lastPath = value;
|
state.lastPath = value;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
metaTitle: "",
|
|
||||||
metaDescription: "",
|
|
||||||
metaKeywords: "",
|
|
||||||
|
|
||||||
metaTags: {
|
metaTags: {
|
||||||
"title": [ "name", "title" ],
|
"title": [ "name", "title" ],
|
||||||
"description": [ "name", "description" ],
|
"description": [ "name", "description" ],
|
||||||
|
@ -21,14 +17,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pageTitle() {
|
|
||||||
return this.metaTitle === "" ? env.appName : `${this.metaTitle} | ${env.appName}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
pageDescription() {
|
|
||||||
return this.metaDescription === "" ? env.appDesc : this.metaDescription;
|
|
||||||
},
|
|
||||||
|
|
||||||
fullPath() {
|
fullPath() {
|
||||||
return document.location.origin + this.$route.path;
|
return document.location.origin + this.$route.path;
|
||||||
}
|
}
|
||||||
|
@ -43,22 +31,22 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMetaData() {
|
updateMetadata(meta) {
|
||||||
let metaContent;
|
let metaContent;
|
||||||
|
|
||||||
document.title = this.pageTitle;
|
document.title = meta.title;
|
||||||
$("link[rel=canonical]").attr("href", this.fullPath);
|
$("link[rel=canonical]").attr("href", this.fullPath);
|
||||||
|
|
||||||
Object.keys(this.metaTags).forEach((name) => {
|
Object.keys(this.metaTags).forEach((name) => {
|
||||||
switch (this.metaTags[name][1]) {
|
switch (this.metaTags[name][1]) {
|
||||||
case "title":
|
case "title":
|
||||||
metaContent = this.pageTitle;
|
metaContent = meta.title;
|
||||||
break;
|
break;
|
||||||
case "description":
|
case "description":
|
||||||
metaContent = this.pageDescription;
|
metaContent = meta.description;
|
||||||
break;
|
break;
|
||||||
case "keywords":
|
case "keywords":
|
||||||
metaContent = this.metaKeywords;
|
metaContent = meta.keywords;
|
||||||
break;
|
break;
|
||||||
case "url":
|
case "url":
|
||||||
metaContent = this.fullPath;
|
metaContent = this.fullPath;
|
||||||
|
@ -69,10 +57,24 @@ export default {
|
||||||
|
|
||||||
this.updateMetaTag(this.metaTags[name][0], name, metaContent);
|
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() {
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
@extends('templates.error', [
|
@extends('templates.error')
|
||||||
'title' => 'Page Not Found'
|
|
||||||
])
|
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ Language::getSessionLanguage() }}">
|
<html lang="{{ Language::getSessionLanguage() }}">
|
||||||
@php
|
@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')));
|
$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
|
@endphp
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
@ -11,22 +18,23 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
<meta name="theme-color" content="#fcfcfc" />
|
<meta name="theme-color" content="#fcfcfc" />
|
||||||
|
|
||||||
<title>{{ $page_title }}</title>
|
<title>{{ $meta['title'] }}</title>
|
||||||
|
|
||||||
<meta name="title" content="{{ $page_title }}" />
|
<meta name="title" content="{{ $meta['title'] }}" />
|
||||||
<meta name="description" content="{{ env('APP_DESC') }}" />
|
<meta name="description" content="{{ $meta['description'] }}" />
|
||||||
<meta name="dc:title" content="{{ $page_title }}" />
|
<meta name="keywords" content="{{ $meta['keywords'] }}" />
|
||||||
<meta name="dc:description" content="{{ env('APP_DESC') }}" />
|
<meta name="dc:title" content="{{ $meta['title'] }}" />
|
||||||
|
<meta name="dc:description" content="{{ $meta['description'] }}" />
|
||||||
|
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:title" content="{{ $page_title }}" />
|
<meta property="og:title" content="{{ $meta['title'] }}" />
|
||||||
<meta property="og:description" content="{{ env('APP_DESC') }}" />
|
<meta property="og:description" content="{{ $meta['description'] }}" />
|
||||||
<meta property="og:url" content="{{ Request::url() }}" />
|
<meta property="og:url" content="{{ Request::url() }}" />
|
||||||
<meta property="og:image" content="{{ asset('/img/logo.png') }}" />
|
<meta property="og:image" content="{{ asset('/img/logo.png') }}" />
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
<meta name="twitter:title" content="{{ $page_title }}" />
|
<meta name="twitter:title" content="{{ $meta['title'] }}" />
|
||||||
<meta name="twitter:description" content="{{ env('APP_DESC') }}" />
|
<meta name="twitter:description" content="{{ $meta['description'] }}" />
|
||||||
<meta name="twitter:image" content="{{ asset('/img/logo.png') }}" />
|
<meta name="twitter:image" content="{{ asset('/img/logo.png') }}" />
|
||||||
|
|
||||||
<link rel="shortcut icon" href="{{ URL::to('/') }}/favicon.ico?version={{ Version::get() }}" />
|
<link rel="shortcut icon" href="{{ URL::to('/') }}/favicon.ico?version={{ Version::get() }}" />
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@extends('templates.base', [ 'title' => 'Dashboard' ])
|
@extends('templates.base')
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$current_page = preg_match('/\/settings$/', Request::url()) ? 'settings' : preg_replace([ '/https?:\/\/[^\/]*\/dashboard\/[^\/]*\//', '/\/.*/' ], [ '', '' ], Request::url());
|
$current_page = preg_match('/\/settings$/', Request::url()) ? 'settings' : preg_replace([ '/https?:\/\/[^\/]*\/dashboard\/[^\/]*\//', '/\/.*/' ], [ '', '' ], Request::url());
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<script>
|
<script>
|
||||||
var env = {
|
var env = {
|
||||||
appName: "{!! env('APP_NAME') !!}",
|
appName: "{!! env('APP_NAME') !!}",
|
||||||
appDesc: "{!! env('APP_DESC') !!}",
|
|
||||||
appLang: "{{ Language::getSessionLanguage() }}",
|
appLang: "{{ Language::getSessionLanguage() }}",
|
||||||
appDefaultLang: "{{ env('DEFAULT_LANGUAGE') }}",
|
appDefaultLang: "{{ env('DEFAULT_LANGUAGE') }}",
|
||||||
apiToken: "{{ Auth::check() ? '?api_token=' . Auth::user()->api_token : '' }}",
|
apiToken: "{{ Auth::check() ? '?api_token=' . Auth::user()->api_token : '' }}",
|
||||||
|
|
|
@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Route;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Route::get('/blog-entries', 'App\Http\Controllers\ApiController@getBlogEntries');
|
Route::get('/blog-entries', 'App\Http\Controllers\ApiController@getBlogEntries');
|
||||||
|
Route::get('/meta/{path?}', 'App\Http\Controllers\ApiController@getMeta');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@extends('templates.public', [ 'title' => 'Blog' ])
|
@extends('templates.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="blog-page-component">
|
<div class="blog-page-component">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@extends('templates.public', [ 'title' => 'Contact' ])
|
@extends('templates.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="contact-page-component">
|
<div class="contact-page-component">
|
||||||
|
|
Loading…
Reference in a new issue