给博客套上 Claen Blog 主题 & 完善博客前台功能
在本节中我们将会为博客添加 Clean Blog 主题,让博客前台看上去更加大气美观。
1、使用 Clean Blog
Clean Blog 是 Start Bootstrap 提供的一个免费博客模板,本节我们将使用该模板美化博客前台页面。
使用 NPM 获取 Clean Blog
首先我们使用 NPM 下载 Clean Blog:
npm install startbootstrap-clean-blog --save-dev
使用 Laravel Mix 管理 Clean Blog
在 resources/sass/app.scss
中引入 Clean Blog 的 Sass 文件:
// Clean Blog
@import "~startbootstrap-clean-blog/scss/clean-blog";
然后运行 npm run dev
重新编译前端资源,新添加的 Clean Blog 的 Sass 资源文件就会通过 Laravel Mix 编译合并到 public/css/app.css
中。
上传顶部背景图片
为了显示博客页面顶部背景图片,我们需要先在后台 http://blog57.test/admin/upload
上传 Clean Blog 提供的四张顶部图片(这些图片位于 node_modules/startbootstrap-clean-blog/img
目录下),我们将这些图片上传到 uploads
目录下:
about-bg.jpg
contact-bg.jpg
home-bg.jpg
post-bg.jpg
2、创建 PostService 服务
我们最后一次接触 BlogController
还是在十分钟创建博客应用那一节,那个时候我们还没有为文章添加标签功能。
如果请求参数中指定了标签,那么我们需要根据该标签来过滤要显示的文章。要实现该功能,我们创建一个独立的服务类来聚合指定标签文章,而不是将业务逻辑一股脑写到控制器中。
首先,在 app/Services
目录下创建一个 PostService
文件,编辑其内容如下:
<?php
namespace App\Services;
use App\Models\Post;
use App\Models\Tag;
use Carbon\Carbon;
class PostService
{
protected $tag;
/**
* 控制器
*
* @param string|null $tag
*/
public function __construct($tag)
{
$this->tag = $tag;
}
public function lists()
{
if ($this->tag) {
return $this->tagIndexData($this->tag);
}
return $this->normalIndexData();
}
/**
* Return data for normal index page
*
* @return array
*/
protected function normalIndexData()
{
$posts = Post::with('tags')
->where('published_at', '<=', Carbon::now())
->where('is_draft', 0)
->orderBy('published_at', 'desc')
->simplePaginate(config('blog.posts_per_page'));
return [
'title' => config('blog.title'),
'subtitle' => config('blog.subtitle'),
'posts' => $posts,
'page_image' => config('blog.page_image'),
'meta_description' => config('blog.description'),
'reverse_direction' => false,
'tag' => null,
];
}
/**
* Return data for a tag index page
*
* @param string $tag
* @return array
*/
protected function tagIndexData($tag)
{
$tag = Tag::where('tag', $tag)->firstOrFail();
$reverse_direction = (bool)$tag->reverse_direction;
$posts = Post::where('published_at', '<=', Carbon::now())
->whereHas('tags', function ($q) use ($tag) {
$q->where('tag', '=', $tag->tag);
})
->where('is_draft', 0)
->orderBy('published_at', $reverse_direction ? 'asc' : 'desc')
->simplePaginate(config('blog.posts_per_page'));
$posts->appends('tag', $tag->tag);
$page_image = $tag->page_image ? : config('blog.page_image');
return [
'title' => $tag->title,
'subtitle' => $tag->subtitle,
'posts' => $posts,
'page_image' => $page_image,
'tag' => $tag,
'reverse_direction' => $reverse_direction,
'meta_description' => $tag->meta_description ?: config('blog.description'),
];
}
}
获取文章列表调用的是 lists
方法,在该方法中,如果传入标签,那么调用 tagIndexData
方法返回根据标签进行过滤的文章列表,否则调用 normalIndexData
返回正常文章列表。
注意到我们返回的数据包含更多字段了吗?在十分钟创建博客应用中我们仅仅返回 $posts
并将其传递到视图,现在我们返回了所有信息。
3、更新控制器 BlogController
修改 BlogController.php
内容如下:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Tag;
use App\Services\PostService;
use Illuminate\Http\Request;
class BlogController extends Controller
{
public function index(Request $request)
{
$tag = $request->get('tag');
$postService = new PostService($tag);
$data = $postService->lists();
$layout = $tag ? Tag::layout($tag) : 'blog.layouts.index';
return view($layout, $data);
}
public function showPost($slug, Request $request)
{
$post = Post::with('tags')->where('slug', $slug)->firstOrFail();
$tag = $request->get('tag');
if ($tag) {
$tag = Tag::where('tag', $tag)->firstOrFail();
}
return view($post->layout, compact('post', 'tag'));
}
}
我们在 index()
中先从请求中获取 $tag
值(没有的话为 null
),然后调用刚刚创建的 PostService
服务来获取文章数据。
showPost()
方法用于显示文章详情,这里我们使用了渴求式加载获取指定文章标签信息。
4、引入前端资源
还有很多事情要做,比如视图创建,但在此之前,我们先引入要用到的前端资源。
编辑 blog.js
在 resources/js/app.js
末尾添加如下代码:
/**
* Blog Javascript
* Copied from Clean Blog v1.0.0 (http://startbootstrap.com)
*/
// Navigation Scripts to Show Header on Scroll-Up
jQuery(document).ready(function ($) {
var MQL = 1170;
//primary navigation slide-in effect
if ($(window).width() > MQL) {
var headerHeight = $('.navbar-custom').height();
$(window).on('scroll', {
previousTop: 0
},
function () {
var currentTop = $(window).scrollTop();
//if user is scrolling up
if (currentTop < this.previousTop) {
if (currentTop > 0 && $('.navbar-custom').hasClass('is-fixed')) {
$('.navbar-custom').addClass('is-visible');
} else {
$('.navbar-custom').removeClass('is-visible is-fixed');
}
//if scrolling down...
} else {
$('.navbar-custom').removeClass('is-visible');
if (currentTop > headerHeight && !$('.navbar-custom').hasClass('is-fixed')) {
$('.navbar-custom').addClass('is-fixed');
}
}
this.previousTop = currentTop;
});
}
// Initialize tooltips
$('[data-toggle="tooltip"]').tooltip();
});
这段代码实现了 tooltips,并且在用户滚动页面时将导航条悬浮在页面顶部。这段代码拷贝自 Clean Blog 的 js/clean-blog.js
文件。
编辑 app.sass
在 resources/sass/app.scss
末尾添加如下代码:
.intro-header .post-heading .meta a, article a {
text-decoration: underline;
}
h2 {
padding-top: 22px;
}
h3 {
padding-top: 15px;
}
h2 + p, h3 + p, h4 + p {
margin-top: 5px;
}
// Adjust position of captions
.caption-title {
margin-bottom: 5px;
}
.caption-title + p {
margin-top: 0;
}
// Change the styling of dt/dd elements
dt {
margin-bottom: 5px;
}
dd {
margin-left: 30px;
margin-bottom: 10px;
}
然后运行 npm run dev
重新编译前端资源,让上述修改生效。
5、创建博客视图
接下来我们来创建用于显示文章列表及详情页的视图。
首先删除十分钟创建博客应用一节中在 resources/views/blog
目录下创建的 index.blade.php
和 post.blade.php
。
创建 blog.layouts.master 视图
在 resources/views/blog
目录下新建 layouts
子目录, 并在该子目录下创建 master.blade.php
布局文件,编辑该文件内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ $meta_description }}">
<meta name="author" content="{{ config('blog.author') }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? config('blog.title') }}</title>
{{-- Styles --}}
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
@yield('styles')
</head>
<body>
@include('blog.partials.page-nav')
@yield('page-header')
@yield('content')
@include('blog.partials.page-footer')
{{-- Scripts --}}
<script src="{{ asset('js/app.js') }}"></script>
@yield('scripts')
</body>
</html>
我们将基于该布局视图实现其它视图。
创建 blog.layouts.index 视图
在 layouts
目录下创建 index.blade.php
视图文件,编辑其内容如下:
@extends('blog.layouts.master')
@section('page-header')
<header class="masthead" style="background-image: url('{{ page_image($page_image) }}')">
<div class="overlay"></div>
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
<div class="site-heading">
<h1>{{ $title }}</h1>
<span class="subheading">{{ $subtitle }}</span>
</div>
</div>
</div>
</div>
</header>
@stop
@section('content')
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
{{-- 文章列表 --}}
@foreach ($posts as $post)
<div class="post-preview">
<a href="{{ $post->url($tag) }}">
<h2 class="post-title">{{ $post->title }}</h2>
@if ($post->subtitle)
<h3 class="post-subtitle">{{ $post->subtitle }}</h3>
@endif
</a>
<p class="post-meta">
Posted on {{ $post->published_at->format('Y-m-d') }}
@if ($post->tags->count())
in
{!! join(', ', $post->tagLinks()) !!}
@endif
</p>
</div>
<hr>
@endforeach
{{-- 分页 --}}
<div class="clearfix">
{{-- Reverse direction --}}
@if ($reverse_direction)
@if ($posts->currentPage() > 1)
<a class="btn btn-primary float-left" href="{!! $posts->url($posts->currentPage() - 1) !!}">
←
Previous {{ $tag->tag }} Posts
</a>
@endif
@if ($posts->hasMorePages())
<a class="btn btn-primary float-right" href="{!! $posts->nextPageUrl() !!}">
Next {{ $tag->tag }} Posts
→
</a>
@endif
@else
@if ($posts->currentPage() > 1)
<a class="btn btn-primary float-left" href="{!! $posts->url($posts->currentPage() - 1) !!}">
←
Newer {{ $tag ? $tag->tag : '' }} Posts
</a>
@endif
@if ($posts->hasMorePages())
<a class="btn btn-primary float-right" href="{!! $posts->nextPageUrl() !!}">
Older {{ $tag ? $tag->tag : '' }} Posts
→
</a>
@endif
@endif
</div>
</div>
</div>
</div>
@stop
该视图用于显示博客首页,其中定义了自己的 page-header
,而 content
部分则循环显示文章列表及分页链接。
创建 blog.layouts.post 视图
接下来我们在 layouts
目录下创建用于显示文章详情的视图文件 post.blade.php
,编辑其内容如下:
@extends('blog.layouts.master', [
'title' => $post->title,
'meta_description' => $post->meta_description ?? config('blog.description'),
])
@section('page-header')
<header class="masthead" style="background-image: url('{{ page_image($post->page_image) }}')">
<div class="overlay"></div>
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
<div class="post-heading">
<h1>{{ $post->title }}</h1>
<h2 class="subheading">{{ $post->subtitle }}</h2>
<span class="meta">
Posted on {{ $post->published_at->format('Y-m-d') }}
@if ($post->tags->count())
in
{!! join(', ', $post->tagLinks()) !!}
@endif
</span>
</div>
</div>
</div>
</div>
</header>
@stop
@section('content')
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
{{-- 文章详情 --}}
<article>
{!! $post->content_html !!}
</article>
<hr>
{{-- 上一篇、下一篇导航 --}}
<div class="clearfix">
{{-- Reverse direction --}}
@if ($tag && $tag->reverse_direction)
@if ($post->olderPost($tag))
<a class="btn btn-primary float-left" href="{!! $post->olderPost($tag)->url($tag) !!}">
←
Previous {{ $tag->tag }} Post
</a>
@endif
@if ($post->newerPost($tag))
<a class="btn btn-primary float-right" href="{!! $post->newerPost($tag)->url($tag) !!}">
Next {{ $tag->tag }} Post
→
</a>
@endif
@else
@if ($post->newerPost($tag))
<a class="btn btn-primary float-left" href="{!! $post->newerPost($tag)->url($tag) !!}">
←
Newer {{ $tag ? $tag->tag : '' }} Post
</a>
@endif
@if ($post->olderPost($tag))
<a class="btn btn-primary float-right" href="{!! $post->olderPost($tag)->url($tag) !!}">
Older {{ $tag ? $tag->tag : '' }} Post
→
</a>
@endif
@endif
</div>
</div>
</div>
</div>
@stop
和 blog.layouts.index
一样,这里也定义了自己的 page-header
和 content
,分别用于渲染文章详情页的页头和文章详情。
创建 blog.partials.page-nav 视图
在 resources/views/blog
目录下新建一个 partials
目录,在该目录中,创建 page-nav.blade.php
并编辑其内容如下:
{{-- Navigation --}}
<nav class="navbar navbar-expand-lg navbar-light fixed-top" id="mainNav">
<div class="container">
{{-- Brand and toggle get grouped for better mobile display --}}
<a class="navbar-brand" href="/">{{ config('blog.name') }}</a>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
导航菜单
<i class="fas fa-bars"></i>
</button>
{{-- Collect the nav links, forms, and other content for toggling --}}
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/">首页</a>
</li>
</ul>
</div>
</div>
</nav>
现在顶部导航条菜单只有一个 —— 「首页」。
创建 blog.partials.page-footer 视图
最后,我们在同一目录下创建 page-footer.blade.php
并编辑其内容如下:
<hr>
<footer>
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<p class="copyright text-muted">Copyright © {{ config('blog.author') }} 2018</p>
</div>
</div>
</div>
</footer>
6、添加模型方法
要让视图能够正常显示,我们还需要新增一些模型方法。
更新 Tag 模型
在 Tag
模型类中新增一个 layout
方法:
/**
* Return the index layout to use for a tag
*
* @param string $tag
* @param string $default
* @return string
*/
public static function layout($tag, $default = 'blog.index')
{
$layout = static::where('tag', $tag)->get()->pluck('layout')->first();
return $layout ?: $default;
}
layout
方法用于返回标签的布局,如果对应标签值不存在或者没有布局,返回默认值。
更新 Post 模型
对 Post
模型类作如下修改:
// 在Post模型类顶部其它use语句下面添加如下这行
use Carbon\Carbon;
// 接着在 Post 模型类中添加如下四个方法
/**
* Return URL to post
*
* @param Tag $tag
* @return string
*/
public function url(Tag $tag = null)
{
$url = url('blog/' . $this->slug);
if ($tag) {
$url .= '?tag=' . urlencode($tag->tag);
}
return $url;
}
/**
* Return array of tag links
*
* @param string $base
* @return array
*/
public function tagLinks($base = '/blog?tag=%TAG%')
{
$tags = $this->tags()->get()->pluck('tag')->all();
$return = [];
foreach ($tags as $tag) {
$url = str_replace('%TAG%', urlencode($tag), $base);
$return[] = '<a href="' . $url . '">' . e($tag) . '</a>';
}
return $return;
}
/**
* Return next post after this one or null
*
* @param Tag $tag
* @return Post
*/
public function newerPost(Tag $tag = null)
{
$query =
static::where('published_at', '>', $this->published_at)
->where('published_at', '<=', Carbon::now())
->where('is_draft', 0)
->orderBy('published_at', 'asc');
if ($tag) {
$query = $query->whereHas('tags', function ($q) use ($tag) {
$q->where('tag', '=', $tag->tag);
});
}
return $query->first();
}
/**
* Return older post before this one or null
*
* @param Tag $tag
* @return Post
*/
public function olderPost(Tag $tag = null)
{
$query =
static::where('published_at', '<', $this->published_at)
->where('is_draft', 0)
->orderBy('published_at', 'desc');
if ($tag) {
$query = $query->whereHas('tags', function ($q) use ($tag) {
$q->where('tag', '=', $tag->tag);
});
}
return $query->first();
}
我们为 Post
模型新增了四个方法。blog.index
视图会使用 url()
方法链接到指定文章详情页。tagLinks()
方法返回一个链接数组,每个链接都会指向首页并带上标签参数。newerPost()
方法返回下一篇文章链接,如果没有的话返回 null
。olderPost()
方法返回前一篇文章链接,如果没有返回 null
。
7、更新博客设置
修改 config/blog.php
文件内容如下:
<?php
return [
'name' => "Laravel 学院",
'title' => "Laravel 学院",
'subtitle' => 'https://laravel.geekai.co',
'description' => 'Laravel学院致力于提供优质Laravel中文学习资源',
'author' => '学院君',
'page_image' => 'home-bg.jpg',
'posts_per_page' => 10,
'uploads' => [
'storage' => 'public',
'webpath' => '/storage/uploads',
],
];
将相应的配置项修改成你自己的配置值,尤其是 uploads
配置。
8、更新示例数据
在十分钟创建博客应用中,我们设置了数据库填充器使用模型工厂生成随机数据。但是现在,数据库改变了。相应的,我们要修改填充器和模型工厂以便重新填充数据库的标签和其它新增字段。
更新数据库填充器
在 database/seeds
目录下有一个 DatabaseSeeder.php
文件,编辑其内容如下:
<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Model::unguard();
$this->call('TagsTableSeeder');
$this->call('PostsTableSeeder');
Model::reguard();
}
}
其中,Model::unguard()
用于取消批量赋值白名单、黑名单属性校验,Model::reguard()
用于恢复校验。然后在同一目录下新建 TagsTableSeeder.php
:
<?php
use Illuminate\Database\Seeder;
use App\Models\Tag;
class TagsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Tag::truncate();
factory(Tag::class, 5)->create();
}
}
然后编辑 PostsTableSeeder.php
内容如下:
<?php
use Illuminate\Database\Seeder;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Support\Facades\DB;
class PostsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Pull all the tag names from the file
$tags = Tag::all()->pluck('tag')->all();
Post::truncate();
// Don't forget to truncate the pivot table
DB::table('post_tag_pivot')->truncate();
factory(Post::class, 20)->create()->each(function ($post) use ($tags) {
// 30% of the time don't assign a tag
if (mt_rand(1, 100) <= 30) {
return;
}
shuffle($tags);
$postTags = [$tags[0]];
// 30% of the time we're assigning tags, assign 2
if (mt_rand(1, 100) <= 30) {
$postTags[] = $tags[1];
}
$post->syncTags($postTags);
});
}
}
最后一个填充器有点长,因为我们还为文章设置了随机标签。
更新模型工厂
接下来更新模型工厂,编辑 database/factories
目录下的 PostFactory.php
内容如下:
<?php
use Faker\Generator as Faker;
use App\Models\Post;
$factory->define(Post::class, function (Faker $faker) {
$images = ['about-bg.jpg', 'contact-bg.jpg', 'home-bg.jpg', 'post-bg.jpg'];
$title = $faker->sentence(mt_rand(3, 10));
return [
'title' => $title,
'subtitle' => str_limit($faker->sentence(mt_rand(10, 20)), 252),
'page_image' => $images[mt_rand(0, 3)],
'content_raw' => join("\n\n", $faker->paragraphs(mt_rand(3, 6))),
'published_at' => $faker->dateTimeBetween('-1 month', '+3 days'),
'meta_description' => "Meta for $title",
'is_draft' => false,
];
});
然后为 Tag
模型创建模型工厂:
php artisan make:factory TagFactory --model=Models/Tag
编辑新生成的 TagFactory
模型工厂文件如下:
<?php
use Faker\Generator as Faker;
use App\Models\Tag;
$factory->define(Tag::class, function (Faker $faker) {
$images = ['about-bg.jpg', 'contact-bg.jpg', 'home-bg.jpg', 'post-bg.jpg'];
$word = $faker->word;
return [
'tag' => $word,
'title' => ucfirst($word),
'subtitle' => $faker->sentence,
'page_image' => $images[mt_rand(0, 3)],
'meta_description' => "Meta for $word",
'reverse_direction' => false,
];
});
填充数据库
最后填充数据库,首先执行如下命令将新增的填充器类加入自动加载文件:
composer dumpauto
然后登录到 Laradock 在项目根目录下运行填充命令将测试数据填充到数据库:
php artisan db:seed
9、访问博客首页及详情页
至此,博客前后端功能基本完成,访问 http://blog57.test
,页面显示如下:
瞬间高大上了有木有?再次访问我们上一节使用 Markdown 格式编辑发布的文章,已经可以正常解析出来了:
好了,至此我们的博客应用开发基本完成,这已经具备一个常见博客的基本功能,并且还有着看上去还不错的外观。后面还有两节,我们将继续为博客应用锦上添花,实现联系我们、邮件队列、RSS订阅、站点地图、博客评论及分享等功能。
23 Comments
Call to undefined method App\Services\PostService::lists() 怎么办?
Sorry 漏掉了 已经补上了
引入 Clean Blog重新编译前端资源发现后台样式被CleanBlog的给覆盖了,
教程5中
教程7中
教程5中
/uploads我说小程序教程中图片怎么404- -引入 Clean Blog重新编译前端资源发现后台样式被CleanBlog的给覆盖了,@学院君 怎样设置一下啊
那么又要在主页显示图片,又能在后台预览,应该怎么写呢,我现在加了uploads,后台就不能预览了.
@ 学院君 我这边前台的界面完全不一样,图片显示不完全,标题和副标题也没有居中,字体和大小还有颜色也完全不一样.到底是哪里出问题了呢,另外如果直接下载github上的代码怎么套用呢,我下载下来进去完全白屏什么都不显示.
前端资源编译过没有
npm install startbootstrap-clean-blog --save-dev下载了clean blog ,也npm run dev编译了,但是文件夹并 没有图片
node_modules/startbootstrap-clean-blog/img
这个目录下没有图片?