在本节中我们将会为博客换个主题,让博客前台看上去更加大气美观。
Clean Blog 是 Start Bootstrap 提供的一个免费博客模板,本节我们将使用该模板美化博客前台页面。
首先我们使用 Bower 下载 Clean Blog:
bower install clean-blog --save
编辑 gulpfile.js
,在 copyfiles
任务底部添加如下这段代码:
// Copy clean-blog less files gulp.src("vendor/bower_dl/clean-blog/less/**") .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
):
最后一次接触 BlogController
还是在十分钟创建博客应用那一节,那个时候我们还没有为文章添加标签功能。
如果请求参数中指定了标签,那么我们需要根据该标签来过滤要显示的文章。要实现该功能,我们创建一个独立的任务来聚合指定标签文章,而不是将业务逻辑一股脑写到控制器中。
首先,使用 Artisan 命令创建一个任务类:
php artisan make:job BlogIndexData
现在,app/Jobs
目录下会新增一个 BlogIndexData.php
文件,编辑其内容如下:
<?php namespace App\Jobs; use App\Post; use App\Tag; use Carbon\Carbon; use Illuminate\Contracts\Bus\SelfHandling; class BlogIndexData extends Job implements SelfHandling { protected $tag; /** * 控制器 * * @param string|null $tag */ public function __construct($tag) { $this->tag = $tag; } /** * Execute the command. * * @return array */ public function handle() { 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->addQuery('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'), ]; } }
任务执行时调用的是 handle
方法,在该方法中,如果传入标签,那么调用 tagIndexData
方法返回根据标签进行过滤的文章列表,否则调用 normalIndexData
返回正常文章列表。
注意到我们返回的数据包含更多字段了吗?在十分钟创建博客应用中我们仅仅返回 $posts
并将其传递到视图,现在我们返回了所有信息。
修改 BlogController.php
内容如下:
<?php namespace App\Http\Controllers; use App\Jobs\BlogIndexData; use App\Http\Requests; use App\Post; use App\Tag; use Illuminate\Http\Request; class BlogController extends Controller { public function index(Request $request) { $tag = $request->get('tag'); $data = $this->dispatch(new BlogIndexData($tag)); $layout = $tag ? Tag::layout($tag) : 'blog.layouts.index'; return view($layout, $data); } public function showPost($slug, Request $request) { $post = Post::with('tags')->whereSlug($slug)->firstOrFail(); $tag = $request->get('tag'); if ($tag) { $tag = Tag::whereTag($tag)->firstOrFail(); } return view($post->layout, compact('post', 'tag')); } }
我们在 index()
中先从请求中获取 $tag
值(没有的话为 null
),然后调用刚刚创建的 BlogIndexData
任务来获取文章数据。
showPost()
方法用于显示文章详情,这里我们使用了渴求式加载获取指定文章标签信息。
还有很多事情要做,比如视图创建,但在此之前,我们先引入要用到的前端资源。
在 resources/assets/js
目录下新建 blog.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
文件。
在 resources/assets/less
目录下新建 blog.less
并编辑其内容如下:
@import "bootstrap/bootstrap"; @import "fontawesome/font-awesome"; @import "clean-blog/clean-blog"; @import "//fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic"; @import "//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,\ 600italic,700italic,800italic,400,300,600,700,800'"; .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; }
这段代码组合了 Bootstrap、Font Awesome 和 Clean Blog。
修改 gulpfile.js
内容如下:
var gulp = require('gulp'); var rename = require('gulp-rename'); var elixir = require('laravel-elixir'); /** * Copy any needed files. * * Do a 'gulp copyfiles' after bower updates */ gulp.task("copyfiles", function() { // Copy jQuery, Bootstrap, and FontAwesome gulp.src("vendor/bower_dl/jquery/dist/jquery.js") .pipe(gulp.dest("resources/assets/js/")); gulp.src("vendor/bower_dl/bootstrap/less/**") .pipe(gulp.dest("resources/assets/less/bootstrap")); gulp.src("vendor/bower_dl/bootstrap/dist/js/bootstrap.js") .pipe(gulp.dest("resources/assets/js/")); gulp.src("vendor/bower_dl/bootstrap/dist/fonts/**") .pipe(gulp.dest("public/assets/fonts")); gulp.src("vendor/bower_dl/fontawesome/less/**") .pipe(gulp.dest("resources/assets/less/fontawesome")); gulp.src("vendor/bower_dl/fontawesome/fonts/**") .pipe(gulp.dest("public/assets/fonts")); // Copy datatables var dtDir = 'vendor/bower_dl/datatables-plugins/integration/'; gulp.src("vendor/bower_dl/datatables/media/js/jquery.dataTables.js") .pipe(gulp.dest('resources/assets/js/')); gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.css') .pipe(rename('dataTables.bootstrap.less')) .pipe(gulp.dest('resources/assets/less/others/')); gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.js') .pipe(gulp.dest('resources/assets/js/')); // Copy selectize gulp.src("vendor/bower_dl/selectize/dist/css/**") .pipe(gulp.dest("public/assets/selectize/css")); gulp.src("vendor/bower_dl/selectize/dist/js/standalone/selectize.min.js") .pipe(gulp.dest("public/assets/selectize/")); // Copy pickadate gulp.src("vendor/bower_dl/pickadate/lib/compressed/themes/**") .pipe(gulp.dest("public/assets/pickadate/themes/")); gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.js") .pipe(gulp.dest("public/assets/pickadate/")); gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.date.js") .pipe(gulp.dest("public/assets/pickadate/")); gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.time.js") .pipe(gulp.dest("public/assets/pickadate/")); // Copy clean-blog less files gulp.src("vendor/bower_dl/clean-blog/less/**") .pipe(gulp.dest("resources/assets/less/clean-blog")); }); /** * Default gulp is to run this elixir stuff */ elixir(function(mix) { // Combine scripts mix.scripts([ 'js/jquery.js', 'js/bootstrap.js', 'js/jquery.dataTables.js', 'js/dataTables.bootstrap.js' ], 'public/assets/js/admin.js', 'resources//assets'); // Combine blog scripts mix.scripts([ 'js/jquery.js', 'js/bootstrap.js', 'js/blog.js' ], 'public/assets/js/blog.js', 'resources//assets'); // Compile CSS mix.less('admin.less', 'public/assets/css/admin.css'); mix.less('blog.less', 'public/assets/css/blog.css'); });
最后运行两次 gulp 命令:
gulp copyfiles gulp
接下来我们来创建用于显示文章列表及详情页的视图。
首先删除十分钟创建博客应用一节中在 resources/views/blog
目录下创建的 index.blade.php
和 post.blade.php
。
在 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') }}">
<title>{{ $title or config('blog.title') }}</title>
{{-- Styles --}}
<link href="/assets/css/blog.css" rel="stylesheet">
@yield('styles')
{{-- HTML5 Shim and Respond.js for IE8 support --}}
<!--[if lt IE 9]>
<script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
@include('blog.partials.page-nav')
@yield('page-header')
@yield('content')
@include('blog.partials.page-footer')
{{-- Scripts --}}
<script src="/assets/js/blog.js"></script>
@yield('scripts')
</body>
</html>
我们将基于该布局视图实现其它视图。
在同一目录下创建 index.blade.php
视图文件,编辑其内容如下:
@extends('blog.layouts.master')
@section('page-header')
<header class="intro-header"
style="background-image: url('{{ page_image($page_image) }}')">
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div class="site-heading">
<h1>{{ $title }}</h1>
<hr class="small">
<h2 class="subheading">{{ $subtitle }}</h2>
</div>
</div>
</div>
</div>
</header>
@stop
@section('content')
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
{{-- 文章列表 --}}
@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('F j, Y') }}
@if ($post->tags->count())
in
{!! join(', ', $post->tagLinks()) !!}
@endif
</p>
</div>
<hr>
@endforeach
{{-- 分页 --}}
<ul class="pager">
{{-- Reverse direction --}}
@if ($reverse_direction)
@if ($posts->currentPage() > 1)
<li class="previous">
<a href="{!! $posts->url($posts->currentPage() - 1) !!}">
<i class="fa fa-long-arrow-left fa-lg"></i>
Previous {{ $tag->tag }} Posts
</a>
</li>
@endif
@if ($posts->hasMorePages())
<li class="next">
<a href="{!! $posts->nextPageUrl() !!}">
Next {{ $tag->tag }} Posts
<i class="fa fa-long-arrow-right"></i>
</a>
</li>
@endif
@else
@if ($posts->currentPage() > 1)
<li class="previous">
<a href="{!! $posts->url($posts->currentPage() - 1) !!}">
<i class="fa fa-long-arrow-left fa-lg"></i>
Newer {{ $tag ? $tag->tag : '' }} Posts
</a>
</li>
@endif
@if ($posts->hasMorePages())
<li class="next">
<a href="{!! $posts->nextPageUrl() !!}">
Older {{ $tag ? $tag->tag : '' }} Posts
<i class="fa fa-long-arrow-right"></i>
</a>
</li>
@endif
@endif
</ul>
</div>
</div>
</div>
@stop
该视图用于显示博客首页,其中定义了自己的 page-header
,而 content
部分则循环显示文章列表。
接下来我们将会创建用于显示文章详情的视图,我们在 resources/views/blog/layouts
目录下新建 post.blade.php
,并编辑其内容如下:
@extends('blog.layouts.master', [
'title' => $post->title,
'meta_description' => $post->meta_description ?: config('blog.description'),
])
@section('page-header')
<header class="intro-header"
style="background-image: url('{{ page_image($post->page_image) }}')">
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div class="post-heading">
<h1>{{ $post->title }}</h1>
<h2 class="subheading">{{ $post->subtitle }}</h2>
<span class="meta">
Posted on {{ $post->published_at->format('F j, Y') }}
@if ($post->tags->count())
in
{!! join(', ', $post->tagLinks()) !!}
@endif
</span>
</div>
</div>
</div>
</div>
</header>
@stop
@section('content')
{{-- The Post --}}
<article>
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
{!! $post->content_html !!}
</div>
</div>
</div>
</article>
{{-- The Pager --}}
<div class="container">
<div class="row">
<ul class="pager">
@if ($tag && $tag->reverse_direction)
@if ($post->olderPost($tag))
<li class="previous">
<a href="{!! $post->olderPost($tag)->url($tag) !!}">
<i class="fa fa-long-arrow-left fa-lg"></i>
Previous {{ $tag->tag }} Post
</a>
</li>
@endif
@if ($post->newerPost($tag))
<li class="next">
<a href="{!! $post->newerPost($tag)->url($tag) !!}">
Next {{ $tag->tag }} Post
<i class="fa fa-long-arrow-right"></i>
</a>
</li>
@endif
@else
@if ($post->newerPost($tag))
<li class="previous">
<a href="{!! $post->newerPost($tag)->url($tag) !!}">
<i class="fa fa-long-arrow-left fa-lg"></i>
Next Newer {{ $tag ? $tag->tag : '' }} Post
</a>
</li>
@endif
@if ($post->olderPost($tag))
<li class="next">
<a href="{!! $post->olderPost($tag)->url($tag) !!}">
Next Older {{ $tag ? $tag->tag : '' }} Post
<i class="fa fa-long-arrow-right"></i>
</a>
</li>
@endif
@endif
</ul>
</div>
</div>
@stop
和 blog.layouts.index
一样,这里也定义了自己的 page-header
和 content
。
在 resources/views/blog
目录下新建一个 partials
目录,在该目录中,创建 page-nav.blade.php
并编辑其内容如下:
{{-- Navigation --}}
<nav class="navbar navbar-default navbar-custom navbar-fixed-top">
<div class="container-fluid">
{{-- Brand and toggle get grouped for better mobile display --}}
<div class="navbar-header page-scroll">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target="#navbar-main">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">{{ config('blog.name') }}</a>
</div>
{{-- Collect the nav links, forms, and other content for toggling --}}
<div class="collapse navbar-collapse" id="navbar-main">
<ul class="nav navbar-nav">
<li>
<a href="/">Home</a>
</li>
</ul>
</div>
</div>
</nav>
现在顶部导航条菜单只有一个 —— Home。
最后,我们在同一目录下创建 page-footer.blade.php
并编辑其内容如下:
<hr>
<footer>
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<p class="copyright">Copyright © {{ config('blog.author') }}</p>
</div>
</div>
</div>
</footer>
要让视图能够正常显示,我们还需要新增一些模型方法。
在 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.layouts.index')
{
$layout = static::whereTag($tag)->pluck('layout');
return $layout ?: $default;
}
layout
方法用于返回 Tag 的布局,如果 tag
不存在或者没有布局,返回默认值。
对 Post
模型类作如下修改:
<?php
// 在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()->lists('tag');
$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.layouts.index
视图会使用 url()
方法链接到指定文章详情页。tagLinks()
方法返回一个链接数组,每个链接都会指向首页并带上标签参数。newerPost()
方法返回下一篇文章链接,如果没有的话返回 null
。olderPost()
方法返回前一篇文章链接,如果没有返回 null
。
修改 config/blog.php
文件内容如下:
<?php
return [
'name' => "Laravel 学院",
'title' => "Laravel 学院",
'subtitle' => 'http://laravelacademy.org',
'description' => 'Laravel学院致力于提供优质Laravel中文学习资源',
'author' => '学院君',
'page_image' => 'home-bg.jpg',
'posts_per_page' => 10,
'uploads' => [
'storage' => 'local',
'webpath' => '/uploads/',
],
];
将相应的配置项修改成你自己的配置值,尤其是 uploads
配置。
在十分钟创建博客应用中,我们设置了数据库填充器使用模型工厂生成随机数据。但是现在,数据库改变了。相应的,我们要修改填充器和模型工厂以便重新填充数据库的标签和其它新增字段。
在 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('TagTableSeeder');
$this->call('PostTableSeeder');
Model::reguard();
}
}
在同一目录下新建 TagTableSeeder.php
:
<?php
use App\Tag;
use Illuminate\Database\Seeder;
class TagTableSeeder extends Seeder
{
/**
* Seed the tags table
*/
public function run()
{
Tag::truncate();
factory(Tag::class, 5)->create();
}
}
然后新建 PostTableSeeder.php
:
<?php
use App\Post;
use App\Tag;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class PostTableSeeder extends Seeder
{
/**
* Seed the posts table
*/
public function run()
{
// Pull all the tag names from the file
$tags = Tag::lists('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
目录下的 ModelFactory.php
内容如下:
<?php
$factory->define(App\User::class, function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => str_random(10),
'remember_token' => str_random(10),
];
});
$factory->define(App\Post::class, function ($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,
];
});
$factory->define(App\Tag::class, function ($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
然后登录到 Homestead 虚拟机在项目根目录下运行填充命令:
php artisan db:seed
至此,博客前后端功能基本完成,访问 http://blog.app/blog
,页面显示如下:
瞬间高大上了有木有?再次访问我们上一节使用 Markdown 格式编辑发布的文章,已经可以正常解析出来了:
好了,至此我们的博客应用开发基本完成,这已经具备一个常见博客的基本功能,并且还有着看上去还不错的外观。后面还有两节,我们将继续为博客应用锦上添花,实现联系我们、邮件队列、RSS订阅、站点地图、博客评论及分享等功能。