基于Laravel开发博客应用系列 —— 前台功能优化:给博客换上漂亮的主题 & 完善博客功能


在本节中我们将会为博客换个主题,让博客前台看上去更加大气美观。

1、使用 Clean Blog

Clean Blog 是 Start Bootstrap 提供的一个免费博客模板,本节我们将使用该模板美化博客前台页面。

使用 Bower 获取Clean Blog

首先我们使用 Bower 下载 Clean Blog:

  1. bower install clean-blog --save

使用 Gulp 管理 Clean Blog 的 Less 文件

编辑 gulpfile.js,在 copyfiles 任务底部添加如下这段代码:

  1. // Copy clean-blog less files
  2. gulp.src("vendor/bower_dl/clean-blog/less/**")
  3. .pipe(gulp.dest("resources/assets/less/clean-blog"));

然后运行 gulp copyfiles,新添加的 clean blog 的资源文件就会被拷贝到 public 目录下。

上传顶部图片

为了显示博客页面顶部图片,我们将在后台 http://blog.app/admin/upload 上传 Clean Blog 提供的四张顶部图片(这些图片位于 vendor/bower_dl/clean-blog/img):

  • about-bg.jpg
  • contact-bg.jpg
  • home-bg.jpg
  • post-bg.jpg

Laravel博客后台上传顶部图片

2、创建 BlogIndexData 任务

最后一次接触 BlogController 还是在十分钟创建博客应用那一节,那个时候我们还没有为文章添加标签功能。

如果请求参数中指定了标签,那么我们需要根据该标签来过滤要显示的文章。要实现该功能,我们创建一个独立的任务来聚合指定标签文章,而不是将业务逻辑一股脑写到控制器中。

首先,使用 Artisan 命令创建一个任务类:

  1. php artisan make:job BlogIndexData

现在,app/Jobs 目录下会新增一个 BlogIndexData.php 文件,编辑其内容如下:

  1. <?php
  2.  
  3. namespace App\Jobs;
  4.  
  5. use App\Post;
  6. use App\Tag;
  7. use Carbon\Carbon;
  8. use Illuminate\Contracts\Bus\SelfHandling;
  9.  
  10. class BlogIndexData extends Job implements SelfHandling
  11. {
  12. protected $tag;
  13.  
  14. /**
  15. * 控制器
  16. *
  17. * @param string|null $tag
  18. */
  19. public function __construct($tag)
  20. {
  21. $this->tag = $tag;
  22. }
  23.  
  24. /**
  25. * Execute the command.
  26. *
  27. * @return array
  28. */
  29. public function handle()
  30. {
  31. if ($this->tag) {
  32. return $this->tagIndexData($this->tag);
  33. }
  34.  
  35. return $this->normalIndexData();
  36. }
  37.  
  38. /**
  39. * Return data for normal index page
  40. *
  41. * @return array
  42. */
  43. protected function normalIndexData()
  44. {
  45. $posts = Post::with('tags')
  46. ->where('published_at', '<=', Carbon::now())
  47. ->where('is_draft', 0)
  48. ->orderBy('published_at', 'desc')
  49. ->simplePaginate(config('blog.posts_per_page'));
  50.  
  51. return [
  52. 'title' => config('blog.title'),
  53. 'subtitle' => config('blog.subtitle'),
  54. 'posts' => $posts,
  55. 'page_image' => config('blog.page_image'),
  56. 'meta_description' => config('blog.description'),
  57. 'reverse_direction' => false,
  58. 'tag' => null,
  59. ];
  60. }
  61.  
  62. /**
  63. * Return data for a tag index page
  64. *
  65. * @param string $tag
  66. * @return array
  67. */
  68. protected function tagIndexData($tag)
  69. {
  70. $tag = Tag::where('tag', $tag)->firstOrFail();
  71. $reverse_direction = (bool)$tag->reverse_direction;
  72.  
  73. $posts = Post::where('published_at', '<=', Carbon::now())
  74. ->whereHas('tags', function ($q) use ($tag) {
  75. $q->where('tag', '=', $tag->tag);
  76. })
  77. ->where('is_draft', 0)
  78. ->orderBy('published_at', $reverse_direction ? 'asc' : 'desc')
  79. ->simplePaginate(config('blog.posts_per_page'));
  80. $posts->addQuery('tag', $tag->tag);
  81.  
  82. $page_image = $tag->page_image ?: config('blog.page_image');
  83.  
  84. return [
  85. 'title' => $tag->title,
  86. 'subtitle' => $tag->subtitle,
  87. 'posts' => $posts,
  88. 'page_image' => $page_image,
  89. 'tag' => $tag,
  90. 'reverse_direction' => $reverse_direction,
  91. 'meta_description' => $tag->meta_description ?: config('blog.description'),
  92. ];
  93. }
  94. }

任务执行时调用的是 handle 方法,在该方法中,如果传入标签,那么调用 tagIndexData 方法返回根据标签进行过滤的文章列表,否则调用 normalIndexData 返回正常文章列表。

注意到我们返回的数据包含更多字段了吗?在十分钟创建博客应用中我们仅仅返回 $posts 并将其传递到视图,现在我们返回了所有信息。

3、更新控制器 BlogController

修改 BlogController.php 内容如下:

  1. <?php
  2.  
  3. namespace App\Http\Controllers;
  4.  
  5. use App\Jobs\BlogIndexData;
  6. use App\Http\Requests;
  7. use App\Post;
  8. use App\Tag;
  9. use Illuminate\Http\Request;
  10.  
  11. class BlogController extends Controller
  12. {
  13. public function index(Request $request)
  14. {
  15. $tag = $request->get('tag');
  16. $data = $this->dispatch(new BlogIndexData($tag));
  17. $layout = $tag ? Tag::layout($tag) : 'blog.layouts.index';
  18.  
  19. return view($layout, $data);
  20. }
  21.  
  22. public function showPost($slug, Request $request)
  23. {
  24. $post = Post::with('tags')->whereSlug($slug)->firstOrFail();
  25. $tag = $request->get('tag');
  26. if ($tag) {
  27. $tag = Tag::whereTag($tag)->firstOrFail();
  28. }
  29.  
  30. return view($post->layout, compact('post', 'tag'));
  31. }
  32. }

我们在 index() 中先从请求中获取 $tag 值(没有的话为 null ),然后调用刚刚创建的 BlogIndexData 任务来获取文章数据。

showPost() 方法用于显示文章详情,这里我们使用了渴求式加载获取指定文章标签信息。

4、引入前端资源

还有很多事情要做,比如视图创建,但在此之前,我们先引入要用到的前端资源。

创建 blog.js

resources/assets/js 目录下新建 blog.js,并编辑其内容如下:

  1. /*
  2. * Blog Javascript
  3. * Copied from Clean Blog v1.0.0 (http://startbootstrap.com)
  4. */
  5.  
  6. // Navigation Scripts to Show Header on Scroll-Up
  7. jQuery(document).ready(function($) {
  8. var MQL = 1170;
  9.  
  10. //primary navigation slide-in effect
  11. if ($(window).width() > MQL) {
  12. var headerHeight = $('.navbar-custom').height();
  13. $(window).on('scroll', {
  14. previousTop: 0
  15. },
  16. function() {
  17. var currentTop = $(window).scrollTop();
  18.  
  19. //if user is scrolling up
  20. if (currentTop < this.previousTop) {
  21. if (currentTop > 0 && $('.navbar-custom').hasClass('is-fixed')) {
  22. $('.navbar-custom').addClass('is-visible');
  23. } else {
  24. $('.navbar-custom').removeClass('is-visible is-fixed');
  25. }
  26. //if scrolling down...
  27. } else {
  28. $('.navbar-custom').removeClass('is-visible');
  29. if (currentTop > headerHeight && !$('.navbar-custom').hasClass('is-fixed')) {
  30. $('.navbar-custom').addClass('is-fixed');
  31. }
  32. }
  33. this.previousTop = currentTop;
  34. });
  35. }
  36.  
  37. // Initialize tooltips
  38. $('[data-toggle="tooltip"]').tooltip();
  39. });

这段代码实现了tooltips,并且在用户滚动页面时将导航条悬浮在页面顶部。这段代码拷贝自 Clean Blog 的 js/clean-blog.js 文件。

创建 blog.less

resources/assets/less 目录下新建 blog.less 并编辑其内容如下:

  1. @import "bootstrap/bootstrap";
  2. @import "fontawesome/font-awesome";
  3. @import "clean-blog/clean-blog";
  4.  
  5. @import "//fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic";
  6. @import "//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,\
  7. 600italic,700italic,800italic,400,300,600,700,800'";
  8.  
  9. .intro-header .post-heading .meta a, article a {
  10. text-decoration: underline;
  11. }
  12.  
  13. h2 {
  14. padding-top: 22px;
  15. }
  16. h3 {
  17. padding-top: 15px;
  18. }
  19.  
  20. h2 + p, h3 + p, h4 + p {
  21. margin-top: 5px;
  22. }
  23.  
  24. // Adjust position of captions
  25. .caption-title {
  26. margin-bottom: 5px;
  27. }
  28. .caption-title + p {
  29. margin-top: 0;
  30. }
  31.  
  32. // Change the styling of dt/dd elements
  33. dt {
  34. margin-bottom: 5px;
  35. }
  36. dd {
  37. margin-left: 30px;
  38. margin-bottom: 10px;
  39. }

这段代码组合了 Bootstrap、Font Awesome 和 Clean Blog。

修改 gulpfile.js

修改 gulpfile.js 内容如下:

  1. var gulp = require('gulp');
  2. var rename = require('gulp-rename');
  3. var elixir = require('laravel-elixir');
  4.  
  5. /**
  6. * Copy any needed files.
  7. *
  8. * Do a 'gulp copyfiles' after bower updates
  9. */
  10. gulp.task("copyfiles", function() {
  11.  
  12. // Copy jQuery, Bootstrap, and FontAwesome
  13. gulp.src("vendor/bower_dl/jquery/dist/jquery.js")
  14. .pipe(gulp.dest("resources/assets/js/"));
  15.  
  16. gulp.src("vendor/bower_dl/bootstrap/less/**")
  17. .pipe(gulp.dest("resources/assets/less/bootstrap"));
  18.  
  19. gulp.src("vendor/bower_dl/bootstrap/dist/js/bootstrap.js")
  20. .pipe(gulp.dest("resources/assets/js/"));
  21.  
  22. gulp.src("vendor/bower_dl/bootstrap/dist/fonts/**")
  23. .pipe(gulp.dest("public/assets/fonts"));
  24.  
  25. gulp.src("vendor/bower_dl/fontawesome/less/**")
  26. .pipe(gulp.dest("resources/assets/less/fontawesome"));
  27.  
  28. gulp.src("vendor/bower_dl/fontawesome/fonts/**")
  29. .pipe(gulp.dest("public/assets/fonts"));
  30.  
  31. // Copy datatables
  32. var dtDir = 'vendor/bower_dl/datatables-plugins/integration/';
  33.  
  34. gulp.src("vendor/bower_dl/datatables/media/js/jquery.dataTables.js")
  35. .pipe(gulp.dest('resources/assets/js/'));
  36.  
  37. gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.css')
  38. .pipe(rename('dataTables.bootstrap.less'))
  39. .pipe(gulp.dest('resources/assets/less/others/'));
  40.  
  41. gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.js')
  42. .pipe(gulp.dest('resources/assets/js/'));
  43.  
  44. // Copy selectize
  45. gulp.src("vendor/bower_dl/selectize/dist/css/**")
  46. .pipe(gulp.dest("public/assets/selectize/css"));
  47.  
  48. gulp.src("vendor/bower_dl/selectize/dist/js/standalone/selectize.min.js")
  49. .pipe(gulp.dest("public/assets/selectize/"));
  50.  
  51. // Copy pickadate
  52. gulp.src("vendor/bower_dl/pickadate/lib/compressed/themes/**")
  53. .pipe(gulp.dest("public/assets/pickadate/themes/"));
  54.  
  55. gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.js")
  56. .pipe(gulp.dest("public/assets/pickadate/"));
  57.  
  58. gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.date.js")
  59. .pipe(gulp.dest("public/assets/pickadate/"));
  60.  
  61. gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.time.js")
  62. .pipe(gulp.dest("public/assets/pickadate/"));
  63.  
  64. // Copy clean-blog less files
  65. gulp.src("vendor/bower_dl/clean-blog/less/**")
  66. .pipe(gulp.dest("resources/assets/less/clean-blog"));
  67. });
  68.  
  69. /**
  70. * Default gulp is to run this elixir stuff
  71. */
  72. elixir(function(mix) {
  73.  
  74. // Combine scripts
  75. mix.scripts([
  76. 'js/jquery.js',
  77. 'js/bootstrap.js',
  78. 'js/jquery.dataTables.js',
  79. 'js/dataTables.bootstrap.js'
  80. ],
  81. 'public/assets/js/admin.js', 'resources//assets');
  82.  
  83. // Combine blog scripts
  84. mix.scripts([
  85. 'js/jquery.js',
  86. 'js/bootstrap.js',
  87. 'js/blog.js'
  88. ], 'public/assets/js/blog.js', 'resources//assets');
  89.  
  90. // Compile CSS
  91. mix.less('admin.less', 'public/assets/css/admin.css');
  92. mix.less('blog.less', 'public/assets/css/blog.css');
  93. });

最后运行两次 gulp 命令:

  1. gulp copyfiles
  2. gulp

5、创建博客视图

接下来我们来创建用于显示文章列表及详情页的视图。

首先删除十分钟创建博客应用一节中在 resources/views/blog 目录下创建的 index.blade.phppost.blade.php

创建 blog.layouts.master 视图

resources/views/blog 目录下新建 layouts 文件夹,然后在该文件夹下新建 master.blade.php 文件,编辑该文件内容如下:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <meta name="description" content="{{ $meta_description }}">
  8. <meta name="author" content="{{ config('blog.author') }}">
  9. <title>{{ $title or config('blog.title') }}</title>
  10. {{-- Styles --}}
  11. <link href="/assets/css/blog.css" rel="stylesheet">
  12. @yield('styles')
  13. {{-- HTML5 Shim and Respond.js for IE8 support --}}
  14. <!--[if lt IE 9]>
  15. <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
  16. <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
  17. <![endif]-->
  18. </head>
  19. <body>
  20. @include('blog.partials.page-nav')
  21. @yield('page-header')
  22. @yield('content')
  23. @include('blog.partials.page-footer')
  24. {{-- Scripts --}}
  25. <script src="/assets/js/blog.js"></script>
  26. @yield('scripts')
  27. </body>
  28. </html>

我们将基于该布局视图实现其它视图。

创建 blog.layouts.index 视图

在同一目录下创建 index.blade.php 视图文件,编辑其内容如下:

  1. @extends('blog.layouts.master')
  2. @section('page-header')
  3. <header class="intro-header"
  4. style="background-image: url('{{ page_image($page_image) }}')">
  5. <div class="container">
  6. <div class="row">
  7. <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
  8. <div class="site-heading">
  9. <h1>{{ $title }}</h1>
  10. <hr class="small">
  11. <h2 class="subheading">{{ $subtitle }}</h2>
  12. </div>
  13. </div>
  14. </div>
  15. </div>
  16. </header>
  17. @stop
  18. @section('content')
  19. <div class="container">
  20. <div class="row">
  21. <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
  22. {{-- 文章列表 --}}
  23. @foreach ($posts as $post)
  24. <div class="post-preview">
  25. <a href="{{ $post->url($tag) }}">
  26. <h2 class="post-title">{{ $post->title }}</h2>
  27. @if ($post->subtitle)
  28. <h3 class="post-subtitle">{{ $post->subtitle }}</h3>
  29. @endif
  30. </a>
  31. <p class="post-meta">
  32. Posted on {{ $post->published_at->format('F j, Y') }}
  33. @if ($post->tags->count())
  34. in
  35. {!! join(', ', $post->tagLinks()) !!}
  36. @endif
  37. </p>
  38. </div>
  39. <hr>
  40. @endforeach
  41. {{-- 分页 --}}
  42. <ul class="pager">
  43. {{-- Reverse direction --}}
  44. @if ($reverse_direction)
  45. @if ($posts->currentPage() > 1)
  46. <li class="previous">
  47. <a href="{!! $posts->url($posts->currentPage() - 1) !!}">
  48. <i class="fa fa-long-arrow-left fa-lg"></i>
  49. Previous {{ $tag->tag }} Posts
  50. </a>
  51. </li>
  52. @endif
  53. @if ($posts->hasMorePages())
  54. <li class="next">
  55. <a href="{!! $posts->nextPageUrl() !!}">
  56. Next {{ $tag->tag }} Posts
  57. <i class="fa fa-long-arrow-right"></i>
  58. </a>
  59. </li>
  60. @endif
  61. @else
  62. @if ($posts->currentPage() > 1)
  63. <li class="previous">
  64. <a href="{!! $posts->url($posts->currentPage() - 1) !!}">
  65. <i class="fa fa-long-arrow-left fa-lg"></i>
  66. Newer {{ $tag ? $tag->tag : '' }} Posts
  67. </a>
  68. </li>
  69. @endif
  70. @if ($posts->hasMorePages())
  71. <li class="next">
  72. <a href="{!! $posts->nextPageUrl() !!}">
  73. Older {{ $tag ? $tag->tag : '' }} Posts
  74. <i class="fa fa-long-arrow-right"></i>
  75. </a>
  76. </li>
  77. @endif
  78. @endif
  79. </ul>
  80. </div>
  81. </div>
  82. </div>
  83. @stop

该视图用于显示博客首页,其中定义了自己的 page-header,而 content 部分则循环显示文章列表。

创建 blog.layouts.post 视图

接下来我们将会创建用于显示文章详情的视图,我们在 resources/views/blog/layouts 目录下新建 post.blade.php,并编辑其内容如下:

  1. @extends('blog.layouts.master', [
  2. 'title' => $post->title,
  3. 'meta_description' => $post->meta_description ?: config('blog.description'),
  4. ])
  5. @section('page-header')
  6. <header class="intro-header"
  7. style="background-image: url('{{ page_image($post->page_image) }}')">
  8. <div class="container">
  9. <div class="row">
  10. <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
  11. <div class="post-heading">
  12. <h1>{{ $post->title }}</h1>
  13. <h2 class="subheading">{{ $post->subtitle }}</h2>
  14. <span class="meta">
  15. Posted on {{ $post->published_at->format('F j, Y') }}
  16. @if ($post->tags->count())
  17. in
  18. {!! join(', ', $post->tagLinks()) !!}
  19. @endif
  20. </span>
  21. </div>
  22. </div>
  23. </div>
  24. </div>
  25. </header>
  26. @stop
  27. @section('content')
  28. {{-- The Post --}}
  29. <article>
  30. <div class="container">
  31. <div class="row">
  32. <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
  33. {!! $post->content_html !!}
  34. </div>
  35. </div>
  36. </div>
  37. </article>
  38. {{-- The Pager --}}
  39. <div class="container">
  40. <div class="row">
  41. <ul class="pager">
  42. @if ($tag && $tag->reverse_direction)
  43. @if ($post->olderPost($tag))
  44. <li class="previous">
  45. <a href="{!! $post->olderPost($tag)->url($tag) !!}">
  46. <i class="fa fa-long-arrow-left fa-lg"></i>
  47. Previous {{ $tag->tag }} Post
  48. </a>
  49. </li>
  50. @endif
  51. @if ($post->newerPost($tag))
  52. <li class="next">
  53. <a href="{!! $post->newerPost($tag)->url($tag) !!}">
  54. Next {{ $tag->tag }} Post
  55. <i class="fa fa-long-arrow-right"></i>
  56. </a>
  57. </li>
  58. @endif
  59. @else
  60. @if ($post->newerPost($tag))
  61. <li class="previous">
  62. <a href="{!! $post->newerPost($tag)->url($tag) !!}">
  63. <i class="fa fa-long-arrow-left fa-lg"></i>
  64. Next Newer {{ $tag ? $tag->tag : '' }} Post
  65. </a>
  66. </li>
  67. @endif
  68. @if ($post->olderPost($tag))
  69. <li class="next">
  70. <a href="{!! $post->olderPost($tag)->url($tag) !!}">
  71. Next Older {{ $tag ? $tag->tag : '' }} Post
  72. <i class="fa fa-long-arrow-right"></i>
  73. </a>
  74. </li>
  75. @endif
  76. @endif
  77. </ul>
  78. </div>
  79. </div>
  80. @stop

blog.layouts.index 一样,这里也定义了自己的 page-headercontent

创建 blog.partials.page-nav 视图

resources/views/blog 目录下新建一个 partials 目录,在该目录中,创建 page-nav.blade.php 并编辑其内容如下:

  1. {{-- Navigation --}}
  2. <nav class="navbar navbar-default navbar-custom navbar-fixed-top">
  3. <div class="container-fluid">
  4. {{-- Brand and toggle get grouped for better mobile display --}}
  5. <div class="navbar-header page-scroll">
  6. <button type="button" class="navbar-toggle" data-toggle="collapse"
  7. data-target="#navbar-main">
  8. <span class="sr-only">Toggle navigation</span>
  9. <span class="icon-bar"></span>
  10. <span class="icon-bar"></span>
  11. <span class="icon-bar"></span>
  12. </button>
  13. <a class="navbar-brand" href="/">{{ config('blog.name') }}</a>
  14. </div>
  15. {{-- Collect the nav links, forms, and other content for toggling --}}
  16. <div class="collapse navbar-collapse" id="navbar-main">
  17. <ul class="nav navbar-nav">
  18. <li>
  19. <a href="/">Home</a>
  20. </li>
  21. </ul>
  22. </div>
  23. </div>
  24. </nav>

现在顶部导航条菜单只有一个 —— Home。

创建 blog.partials.page-footer 视图

最后,我们在同一目录下创建 page-footer.blade.php 并编辑其内容如下:

  1. <hr>
  2. <footer>
  3. <div class="container">
  4. <div class="row">
  5. <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
  6. <p class="copyright">Copyright © {{ config('blog.author') }}</p>
  7. </div>
  8. </div>
  9. </div>
  10. </footer>

6、添加模型方法

要让视图能够正常显示,我们还需要新增一些模型方法。

更新 Tag 模型

Tag 模型类中新增一个 layout 方法:

  1. /**
  2. * Return the index layout to use for a tag
  3. *
  4. * @param string $tag
  5. * @param string $default
  6. * @return string
  7. */
  8. public static function layout($tag, $default = 'blog.layouts.index')
  9. {
  10. $layout = static::whereTag($tag)->pluck('layout');
  11. return $layout ?: $default;
  12. }

layout 方法用于返回 Tag 的布局,如果 tag 不存在或者没有布局,返回默认值。

更新 Post 模型

Post 模型类作如下修改:

  1. <?php
  2. // 在Post模型类顶部其它use语句下面添加如下这行
  3. use Carbon\Carbon;
  4. // 接着在 Post 模型类中添加如下四个方法
  5. /**
  6. * Return URL to post
  7. *
  8. * @param Tag $tag
  9. * @return string
  10. */
  11. public function url(Tag $tag = null)
  12. {
  13. $url = url('blog/'.$this->slug);
  14. if ($tag) {
  15. $url .= '?tag='.urlencode($tag->tag);
  16. }
  17. return $url;
  18. }
  19. /**
  20. * Return array of tag links
  21. *
  22. * @param string $base
  23. * @return array
  24. */
  25. public function tagLinks($base = '/blog?tag=%TAG%')
  26. {
  27. $tags = $this->tags()->lists('tag');
  28. $return = [];
  29. foreach ($tags as $tag) {
  30. $url = str_replace('%TAG%', urlencode($tag), $base);
  31. $return[] = '<a href="'.$url.'">'.e($tag).'</a>';
  32. }
  33. return $return;
  34. }
  35. /**
  36. * Return next post after this one or null
  37. *
  38. * @param Tag $tag
  39. * @return Post
  40. */
  41. public function newerPost(Tag $tag = null)
  42. {
  43. $query =
  44. static::where('published_at', '>', $this->published_at)
  45. ->where('published_at', '<=', Carbon::now())
  46. ->where('is_draft', 0)
  47. ->orderBy('published_at', 'asc');
  48. if ($tag) {
  49. $query = $query->whereHas('tags', function ($q) use ($tag) {
  50. $q->where('tag', '=', $tag->tag);
  51. });
  52. }
  53. return $query->first();
  54. }
  55. /**
  56. * Return older post before this one or null
  57. *
  58. * @param Tag $tag
  59. * @return Post
  60. */
  61. public function olderPost(Tag $tag = null)
  62. {
  63. $query =
  64. static::where('published_at', '<', $this->published_at)
  65. ->where('is_draft', 0)
  66. ->orderBy('published_at', 'desc');
  67. if ($tag) {
  68. $query = $query->whereHas('tags', function ($q) use ($tag) {
  69. $q->where('tag', '=', $tag->tag);
  70. });
  71. }
  72. return $query->first();
  73. }

我们为 Post 模型新增了四个方法。blog.layouts.index 视图会使用 url() 方法链接到指定文章详情页。tagLinks() 方法返回一个链接数组,每个链接都会指向首页并带上标签参数。newerPost() 方法返回下一篇文章链接,如果没有的话返回 nullolderPost() 方法返回前一篇文章链接,如果没有返回 null

7、更新博客设置

修改 config/blog.php 文件内容如下:

  1. <?php
  2. return [
  3. 'name' => "Laravel 学院",
  4. 'title' => "Laravel 学院",
  5. 'subtitle' => 'http://laravelacademy.org',
  6. 'description' => 'Laravel学院致力于提供优质Laravel中文学习资源',
  7. 'author' => '学院君',
  8. 'page_image' => 'home-bg.jpg',
  9. 'posts_per_page' => 10,
  10. 'uploads' => [
  11. 'storage' => 'local',
  12. 'webpath' => '/uploads/',
  13. ],
  14. ];

将相应的配置项修改成你自己的配置值,尤其是 uploads 配置。

8、更新示例数据

在十分钟创建博客应用中,我们设置了数据库填充器使用模型工厂生成随机数据。但是现在,数据库改变了。相应的,我们要修改填充器和模型工厂以便重新填充数据库的标签和其它新增字段。

更新数据库填充器

database/seeds 目录下有一个 DatabaseSeeder.php 文件,编辑其内容如下:

  1. <?php
  2. use Illuminate\Database\Seeder;
  3. use Illuminate\Database\Eloquent\Model;
  4. class DatabaseSeeder extends Seeder
  5. {
  6. /**
  7. * Run the database seeds.
  8. *
  9. * @return void
  10. */
  11. public function run()
  12. {
  13. Model::unguard();
  14. $this->call('TagTableSeeder');
  15. $this->call('PostTableSeeder');
  16. Model::reguard();
  17. }
  18. }

在同一目录下新建 TagTableSeeder.php

  1. <?php
  2. use App\Tag;
  3. use Illuminate\Database\Seeder;
  4. class TagTableSeeder extends Seeder
  5. {
  6. /**
  7. * Seed the tags table
  8. */
  9. public function run()
  10. {
  11. Tag::truncate();
  12. factory(Tag::class, 5)->create();
  13. }
  14. }

然后新建 PostTableSeeder.php

  1. <?php
  2. use App\Post;
  3. use App\Tag;
  4. use Illuminate\Database\Seeder;
  5. use Illuminate\Support\Facades\DB;
  6. class PostTableSeeder extends Seeder
  7. {
  8. /**
  9. * Seed the posts table
  10. */
  11. public function run()
  12. {
  13. // Pull all the tag names from the file
  14. $tags = Tag::lists('tag')->all();
  15. Post::truncate();
  16. // Don't forget to truncate the pivot table
  17. DB::table('post_tag_pivot')->truncate();
  18. factory(Post::class, 20)->create()->each(function ($post) use ($tags) {
  19. // 30% of the time don't assign a tag
  20. if (mt_rand(1, 100) <= 30) {
  21. return;
  22. }
  23. shuffle($tags);
  24. $postTags = [$tags[0]];
  25. // 30% of the time we're assigning tags, assign 2
  26. if (mt_rand(1, 100) <= 30) {
  27. $postTags[] = $tags[1];
  28. }
  29. $post->syncTags($postTags);
  30. });
  31. }
  32. }

最后一个填充器有点长,因为我们还为文章设置了随机标签。

更新模型工厂

接下来更新模型工厂,编辑 database/factories 目录下的 ModelFactory.php 内容如下:

  1. <?php
  2. $factory->define(App\User::class, function ($faker) {
  3. return [
  4. 'name' => $faker->name,
  5. 'email' => $faker->email,
  6. 'password' => str_random(10),
  7. 'remember_token' => str_random(10),
  8. ];
  9. });
  10. $factory->define(App\Post::class, function ($faker) {
  11. $images = ['about-bg.jpg', 'contact-bg.jpg', 'home-bg.jpg', 'post-bg.jpg'];
  12. $title = $faker->sentence(mt_rand(3, 10));
  13. return [
  14. 'title' => $title,
  15. 'subtitle' => str_limit($faker->sentence(mt_rand(10, 20)), 252),
  16. 'page_image' => $images[mt_rand(0, 3)],
  17. 'content_raw' => join("\n\n", $faker->paragraphs(mt_rand(3, 6))),
  18. 'published_at' => $faker->dateTimeBetween('-1 month', '+3 days'),
  19. 'meta_description' => "Meta for $title",
  20. 'is_draft' => false,
  21. ];
  22. });
  23. $factory->define(App\Tag::class, function ($faker) {
  24. $images = ['about-bg.jpg', 'contact-bg.jpg', 'home-bg.jpg', 'post-bg.jpg'];
  25. $word = $faker->word;
  26. return [
  27. 'tag' => $word,
  28. 'title' => ucfirst($word),
  29. 'subtitle' => $faker->sentence,
  30. 'page_image' => $images[mt_rand(0, 3)],
  31. 'meta_description' => "Meta for $word",
  32. 'reverse_direction' => false,
  33. ];
  34. });

填充数据库

最后填充数据库,首先执行如下命令将新增的填充器类加入自动加载文件:

  1. composer dumpauto

然后登录到 Homestead 虚拟机在项目根目录下运行填充命令:

  1. php artisan db:seed

9、访问博客首页及详情页

至此,博客前后端功能基本完成,访问 http://blog.app/blog,页面显示如下:

Laravel博客首页

瞬间高大上了有木有?再次访问我们上一节使用 Markdown 格式编辑发布的文章,已经可以正常解析出来了:

Laravel博客详情页

好了,至此我们的博客应用开发基本完成,这已经具备一个常见博客的基本功能,并且还有着看上去还不错的外观。后面还有两节,我们将继续为博客应用锦上添花,实现联系我们、邮件队列、RSS订阅、站点地图、博客评论及分享等功能。