用 Gridsome 构建一个静态的博客网站。

创建项目

  1. gridsome create blog-with-gridsome
  2. # 启动项目
  3. npm run develop

main.js 中引入 css 文件:

  1. import 'bootstrap/dist/css/bootstrap.min.css'
  2. import '@fortawesome/fontawesome-free/css/all.min.css'
  3. import './assets/css/index.css'
  4. import DefaultLayout from '~/layouts/Default.vue'
  5. export default function (Vue, { router, head, isClient }) {
  6. // Set default layout as a global component
  7. Vue.component('Layout', DefaultLayout)
  8. }

默认布局组件

  1. <template>
  2. <div class="layout">
  3. <!-- Navigation -->
  4. <nav
  5. class="navbar navbar-expand-lg navbar-light fixed-top"
  6. id="mainNav"
  7. >
  8. <div class="container">
  9. <a
  10. class="navbar-brand"
  11. href="index.html"
  12. >Start Bootstrap</a>
  13. <button
  14. aria-controls="navbarResponsive"
  15. aria-expanded="false"
  16. aria-label="Toggle navigation"
  17. class="navbar-toggler navbar-toggler-right"
  18. data-target="#navbarResponsive"
  19. data-toggle="collapse"
  20. type="button"
  21. >
  22. Menu
  23. <i class="fas fa-bars"></i>
  24. </button>
  25. <div
  26. class="collapse navbar-collapse"
  27. id="navbarResponsive"
  28. >
  29. <ul class="navbar-nav ml-auto">
  30. <li class="nav-item">
  31. <a
  32. class="nav-link"
  33. href="index.html"
  34. >Home</a>
  35. </li>
  36. <li class="nav-item">
  37. <a
  38. class="nav-link"
  39. href="about.html"
  40. >About</a>
  41. </li>
  42. <li class="nav-item">
  43. <a
  44. class="nav-link"
  45. href="post.html"
  46. >Sample Post</a>
  47. </li>
  48. <li class="nav-item">
  49. <a
  50. class="nav-link"
  51. href="contact.html"
  52. >Contact</a>
  53. </li>
  54. </ul>
  55. </div>
  56. </div>
  57. </nav>
  58. <slot />
  59. <!-- Footer -->
  60. <footer>
  61. <div class="container">
  62. <div class="row">
  63. <div class="col-lg-8 col-md-10 mx-auto">
  64. <ul class="list-inline text-center">
  65. <li class="list-inline-item">
  66. <a href="#">
  67. <span class="fa-stack fa-lg">
  68. <i class="fas fa-circle fa-stack-2x"></i>
  69. <i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
  70. </span>
  71. </a>
  72. </li>
  73. <li class="list-inline-item">
  74. <a href="#">
  75. <span class="fa-stack fa-lg">
  76. <i class="fas fa-circle fa-stack-2x"></i>
  77. <i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
  78. </span>
  79. </a>
  80. </li>
  81. <li class="list-inline-item">
  82. <a href="#">
  83. <span class="fa-stack fa-lg">
  84. <i class="fas fa-circle fa-stack-2x"></i>
  85. <i class="fab fa-github fa-stack-1x fa-inverse"></i>
  86. </span>
  87. </a>
  88. </li>
  89. </ul>
  90. <p class="copyright text-muted">Copyright &copy; Your Website 2020</p>
  91. </div>
  92. </div>
  93. </div>
  94. </footer>
  95. </div>
  96. </template>
  97. <style>
  98. </style>

页面组件

Index.vue

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header
  5. class="masthead"
  6. style="background-image: url('/img/home-bg.jpg')"
  7. >
  8. <div class="overlay"></div>
  9. <div class="container">
  10. <div class="row">
  11. <div class="col-lg-8 col-md-10 mx-auto">
  12. <div class="site-heading">
  13. <h1>Clean Blog</h1>
  14. <span class="subheading">A Blog Theme by Start Bootstrap</span>
  15. </div>
  16. </div>
  17. </div>
  18. </div>
  19. </header>
  20. <!-- Main Content -->
  21. <div class="container">
  22. <div class="row">
  23. <div class="col-lg-8 col-md-10 mx-auto">
  24. <div class="post-preview">
  25. <a href="post.html">
  26. <h2 class="post-title">Man must explore, and this is exploration at its greatest</h2>
  27. <h3 class="post-subtitle">Problems look mighty small from 150 miles up</h3>
  28. </a>
  29. <p class="post-meta">
  30. Posted by
  31. <a href="#">Start Bootstrap</a>
  32. on September 24, 2019
  33. </p>
  34. </div>
  35. <hr />
  36. <div class="post-preview">
  37. <a href="post.html">
  38. <h2
  39. class="post-title"
  40. >I believe every human has a finite number of heartbeats. I don't intend to waste any of mine.</h2>
  41. </a>
  42. <p class="post-meta">
  43. Posted by
  44. <a href="#">Start Bootstrap</a>
  45. on September 18, 2019
  46. </p>
  47. </div>
  48. <hr />
  49. <div class="post-preview">
  50. <a href="post.html">
  51. <h2 class="post-title">Science has not yet mastered prophecy</h2>
  52. <h3 class="post-subtitle">We predict too much for the next year and yet far too little for the next ten.</h3>
  53. </a>
  54. <p class="post-meta">
  55. Posted by
  56. <a href="#">Start Bootstrap</a>
  57. on August 24, 2019
  58. </p>
  59. </div>
  60. <hr />
  61. <div class="post-preview">
  62. <a href="post.html">
  63. <h2 class="post-title">Failure is not an option</h2>
  64. <h3
  65. class="post-subtitle"
  66. >Many say exploration is part of our destiny, but it’s actually our duty to future generations.</h3>
  67. </a>
  68. <p class="post-meta">
  69. Posted by
  70. <a href="#">Start Bootstrap</a>
  71. on July 8, 2019
  72. </p>
  73. </div>
  74. <hr />
  75. <!-- Pager -->
  76. <div class="clearfix">
  77. <a
  78. class="btn btn-primary float-right"
  79. href="#"
  80. >Older Posts &rarr;</a>
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. </Layout>
  86. </template>
  87. <script>
  88. export default {
  89. metaInfo: {
  90. title: 'Hello, world!'
  91. },
  92. name: 'HomePage'
  93. }
  94. </script>
  95. <style>
  96. </style>

Post.vue

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header
  5. class="masthead"
  6. style="background-image: url('img/post-bg.jpg')"
  7. >
  8. <div class="overlay"></div>
  9. <div class="container">
  10. <div class="row">
  11. <div class="col-lg-8 col-md-10 mx-auto">
  12. <div class="post-heading">
  13. <h1>Man must explore, and this is exploration at its greatest</h1>
  14. <h2 class="subheading">Problems look mighty small from 150 miles up</h2>
  15. <span class="meta">
  16. Posted by
  17. <a href="#">Start Bootstrap</a>
  18. on August 24, 2019
  19. </span>
  20. </div>
  21. </div>
  22. </div>
  23. </div>
  24. </header>
  25. <!-- Post Content -->
  26. <article>
  27. <div class="container">
  28. <div class="row">
  29. <div class="col-lg-8 col-md-10 mx-auto">
  30. <p>Never in all their history have men been able truly to conceive of the world as one: a single sphere, a globe, having the qualities of a globe, a round earth in which all the directions eventually meet, in which there is no center because every point, or none, is center — an equal earth which all men occupy as equals. The airman's earth, if free men make it, will be truly round: a globe in practice, not in theory.</p>
  31. <p>Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.</p>
  32. <p>What was most significant about the lunar voyage was not that man set foot on the Moon but that they set eye on the earth.</p>
  33. <p>A Chinese tale tells of some men sent to harm a young girl who, upon seeing her beauty, become her protectors rather than her violators. That's how I felt seeing the Earth for the first time. I could not help but love and cherish her.</p>
  34. <p>For those who have seen the Earth from space, and for the hundreds and perhaps thousands more who will, the experience most certainly changes your perspective. The things that we share in our world are far more valuable than those which divide us.</p>
  35. <h2 class="section-heading">The Final Frontier</h2>
  36. <p>There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.</p>
  37. <p>There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.</p>
  38. <blockquote
  39. class="blockquote"
  40. >The dreams of yesterday are the hopes of today and the reality of tomorrow. Science has not yet mastered prophecy. We predict too much for the next year and yet far too little for the next ten.</blockquote>
  41. <p>Spaceflights cannot be stopped. This is not the work of any one man or even a group of men. It is a historical process which mankind is carrying out in accordance with the natural laws of human development.</p>
  42. <h2 class="section-heading">Reaching for the Stars</h2>
  43. <p>As we got further and further away, it [the Earth] diminished in size. Finally it shrank to the size of a marble, the most beautiful you can imagine. That beautiful, warm, living object looked so fragile, so delicate, that if you touched it with a finger it would crumble and fall apart. Seeing this has to change a man.</p>
  44. <a href="#">
  45. <img
  46. alt
  47. class="img-fluid"
  48. src="img/post-sample-image.jpg"
  49. />
  50. </a>
  51. <span
  52. class="caption text-muted"
  53. >To go places and do things that have never been done before – that’s what living is all about.</span>
  54. <p>Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.</p>
  55. <p>As I stand out here in the wonders of the unknown at Hadley, I sort of realize there’s a fundamental truth to our nature, Man must explore, and this is exploration at its greatest.</p>
  56. <p>
  57. Placeholder text by
  58. <a href="http://spaceipsum.com/">Space Ipsum</a>. Photographs by
  59. <a href="https://www.flickr.com/photos/nasacommons/">NASA on The Commons</a>.
  60. </p>
  61. </div>
  62. </div>
  63. </div>
  64. </article>
  65. </Layout>
  66. </template>
  67. <script>
  68. export default {
  69. name: 'PostPage',
  70. }
  71. </script>
  72. <style scoped>
  73. </style>

About.vue

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header
  5. class="masthead"
  6. style="background-image: url('img/about-bg.jpg')"
  7. >
  8. <div class="overlay"></div>
  9. <div class="container">
  10. <div class="row">
  11. <div class="col-lg-8 col-md-10 mx-auto">
  12. <div class="page-heading">
  13. <h1>About Me</h1>
  14. <span class="subheading">This is what I do.</span>
  15. </div>
  16. </div>
  17. </div>
  18. </div>
  19. </header>
  20. <!-- Main Content -->
  21. <div class="container">
  22. <div class="row">
  23. <div class="col-lg-8 col-md-10 mx-auto">
  24. <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe nostrum ullam eveniet pariatur voluptates odit, fuga atque ea nobis sit soluta odio, adipisci quas excepturi maxime quae totam ducimus consectetur?</p>
  25. <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius praesentium recusandae illo eaque architecto error, repellendus iusto reprehenderit, doloribus, minus sunt. Numquam at quae voluptatum in officia voluptas voluptatibus, minus!</p>
  26. <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut consequuntur magnam, excepturi aliquid ex itaque esse est vero natus quae optio aperiam soluta voluptatibus corporis atque iste neque sit tempora!</p>
  27. </div>
  28. </div>
  29. </div>
  30. </Layout>
  31. </template>
  32. <script>
  33. export default {
  34. metaInfo: {
  35. title: 'About us'
  36. },
  37. name: 'AboutPage',
  38. }
  39. </script>

Contact.vue

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header class="masthead" style="background-image: url('/img/contact-bg.jpg')">
  5. <div class="overlay"></div>
  6. <div class="container">
  7. <div class="row">
  8. <div class="col-lg-8 col-md-10 mx-auto">
  9. <div class="page-heading">
  10. <h1>Contact Me</h1>
  11. <span class="subheading">Have questions? I have answers.</span>
  12. </div>
  13. </div>
  14. </div>
  15. </div>
  16. </header>
  17. <!-- Main Content -->
  18. <div class="container">
  19. <div class="row">
  20. <div class="col-lg-8 col-md-10 mx-auto">
  21. <p>Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as possible!</p>
  22. <!-- Contact Form - Enter your email address on line 19 of the mail/contact_me.php file to make this form work. -->
  23. <!-- WARNING: Some web hosts do not allow emails to be sent through forms to common mail hosts like Gmail or Yahoo. It's recommended that you use a private domain email address! -->
  24. <!-- To use the contact form, your site must be on a live web host with PHP! The form will not work locally! -->
  25. <form name="sentMessage" id="contactForm" novalidate>
  26. <div class="control-group">
  27. <div class="form-group floating-label-form-group controls">
  28. <label>Name</label>
  29. <input type="text" class="form-control" placeholder="Name" id="name" required data-validation-required-message="Please enter your name.">
  30. <p class="help-block text-danger"></p>
  31. </div>
  32. </div>
  33. <div class="control-group">
  34. <div class="form-group floating-label-form-group controls">
  35. <label>Email Address</label>
  36. <input type="email" class="form-control" placeholder="Email Address" id="email" required data-validation-required-message="Please enter your email address.">
  37. <p class="help-block text-danger"></p>
  38. </div>
  39. </div>
  40. <div class="control-group">
  41. <div class="form-group col-xs-12 floating-label-form-group controls">
  42. <label>Phone Number</label>
  43. <input type="tel" class="form-control" placeholder="Phone Number" id="phone" required data-validation-required-message="Please enter your phone number.">
  44. <p class="help-block text-danger"></p>
  45. </div>
  46. </div>
  47. <div class="control-group">
  48. <div class="form-group floating-label-form-group controls">
  49. <label>Message</label>
  50. <textarea rows="5" class="form-control" placeholder="Message" id="message" required data-validation-required-message="Please enter a message."></textarea>
  51. <p class="help-block text-danger"></p>
  52. </div>
  53. </div>
  54. <br>
  55. <div id="success"></div>
  56. <button type="submit" class="btn btn-primary" id="sendMessageButton">Send</button>
  57. </form>
  58. </div>
  59. </div>
  60. </div>
  61. </Layout>
  62. </template>
  63. <script>
  64. export default {
  65. name: 'ContactPage'
  66. }
  67. </script>
  68. <style>
  69. </style>

读取数据

Gridsome 有很多获取数据的方式,可以通过读取本地的 markdown 文件来实现。
安装插件:

  1. # 读取文件插件
  2. yarn add @gridsome/source-filesystem
  3. # 文件转换器
  4. yarn add @gridsome/transformer-remark

gridsome.config.js中配置插件:

  1. module.exports = {
  2. siteName: 'Gridsome',
  3. plugins: [
  4. {
  5. use: '@gridsome/source-filesystem',
  6. options: {
  7. path: './content/blog/**/*.md',
  8. typeName: 'BlogPost'
  9. }
  10. }
  11. ]
  12. }

应用启动后,插件就会自动把文件中的内容加载到 GraphQL。

Strapi

strapi 是一个先进的 Node.js 内容管理框架(headless-CMS),可以毫不费力地构建强大的 API 。

功能特性

  • 现代化管理面板:优雅、完全可定制、完全可扩展的管理面板。
  • 默认安全:可重用策略、CSRF、CORS、P3P、Xframe、XSS 等等。
  • 插件化:可在几秒钟内安装身份验证系统、内容管理、自定义插件等等。
  • 极速:基于 Node.js,Strapi 表现惊人。
  • 前端不可知论者(Front-end Agnostic): 可使用任何前端框架(React、Vue、Angular等)、移动应用,甚至是物联网。
  • 强大的 CLI:脚手架项目和 API 不停机操作 。
  • SQL & NoSQL 数据库:使用 Mongo 作为主数据库,同时支持 Postgres、MySQL 等。

安装

如果本地安装了 npm 和 Node.js,可以通过命令行直接创建。
官方推荐使用的 npm 版本为 v6,Node.js 仅限 LTS 版本,推荐 12或14。
现在默认安装的是 strapi v4,如果要使用 v3,需要指定版本号。

  1. # 使用最新版 strapi 创建一个名为 my-project 的项目
  2. npx create-strapi-app@latest my-project
  3. # 安装 3.x 版本
  4. npx create-strapi-app@3.1.4 my-project
  5. npx create-strapi-app@3.6.8 my-project

安装完成后会自动运行,如果没有自动运行使用npm run develop把项目跑起来。
项目跑起来后会自动在浏览器打开管理页,如果没有自动打开,在浏览器输入 http://localhost:1337/admin 即可访问。
第一次使用需要注册账号,之后使用邮箱和密码登录。

数据管理

Strapi 默认生成了一些用户角色权限的集合。如果要添加集合,在 插件 -> 内容类型生成器里添加。创建集合时用户只需要添加业务相关的字段,id 和 创建时间、更新时间的字段会自动添加,新版本还会多添加一个发布时间。
image.png

回到 COLLECTION TYPES 对数据增删改查。新旧版本管理数据步骤稍有不同,新版本的 Strapi 如v3.6数据保存后,如果需要暴露出去,还要点击Publish,否则更改后的数据是只能在管理页看到的草稿。
image.png

数据访问

Strapi 默认提供了符合 Restful 规范的 API。

角色和权限

新旧版本管理角色和权限的地方也有所变化,新版本的集成到了设置当中。
Strapi 默认添加了两个角色,AuthenticatedPublic,但是没有给它们分配访问接口的权限。
Public是公共角色,应该分配一些不需要验证用户信息的查询接口,Authenticated则是需要验证 token 的角色,可以按需分配增删改查的权限。
image.png

Public角色分配了查询权限以后,就能调用接口查询了,比如查询 Post 集合的列表:
image.png

访问受限接口

操作数据的接口开放给Authenticated角色,然后新建一个用户并把Authenticated角色赋给该用户。
Blocked打开表示用户被锁定,所以不要选,Confirmed表示用户是否已确认,这个无所谓。
image.png

如果是普通用户在界面申请注册账号,那么调用接口注册。

  1. import axios from 'axios';
  2. // Request API.
  3. // Add your own code here to customize or restrict how the public can register new users.
  4. axios
  5. .post('http://localhost:1337/auth/local/register', {
  6. username: 'Strapi user',
  7. email: 'user@strapi.io',
  8. password: 'strapiPassword',
  9. })
  10. .then(response => {
  11. // Handle success.
  12. console.log('Well done!');
  13. console.log('User profile', response.data.user);
  14. console.log('User token', response.data.jwt);
  15. })
  16. .catch(error => {
  17. // Handle error.
  18. console.log('An error occurred:', error.response);
  19. });

账号创建了之后,就可以拿着账号密码发出登录请求了。

  1. import axios from 'axios';
  2. // Request API.
  3. axios
  4. .post('http://localhost:1337/auth/local', {
  5. identifier: 'user@strapi.io', // email or username
  6. password: 'strapiPassword',
  7. })
  8. .then(response => {
  9. // Handle success.
  10. console.log('Well done!');
  11. console.log('User profile', response.data.user);
  12. console.log('User token', response.data.jwt);
  13. })
  14. .catch(error => {
  15. // Handle error.
  16. console.log('An error occurred:', error.response);
  17. });

发出请求后返回 jwt 和 用户信息:

  1. {
  2. "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjQ0NzIwNzM5LCJleHAiOjE2NDczMTI3Mzl9.I8xvBRXY_uCg_L7f4r8gXAzL9IrVQNW1jSedz8bARLI",
  3. "user": {
  4. "id": 1,
  5. "username": "yanlin",
  6. "email": "ylcresister@gmail.com",
  7. "provider": "local",
  8. "confirmed": false,
  9. "blocked": false,
  10. "role": {
  11. "id": 1,
  12. "name": "Authenticated",
  13. "description": "Default role given to authenticated user.",
  14. "type": "authenticated"
  15. },
  16. "created_at": "2022-02-13T02:44:59.350Z",
  17. "updated_at": "2022-02-13T02:44:59.358Z"
  18. }
  19. }

现在来添加一个 post 数据,最重要的是把 jwt 设置到请求头中。

  1. import axios from 'axios';
  2. const token = 'YOUR_TOKEN_HERE';
  3. // Request API.
  4. axios
  5. .post('http://localhost:1337/posts', {
  6. headers: {
  7. Authorization: `Bearer ${token}`,
  8. },
  9. })
  10. .then(response => {
  11. // Handle success.
  12. console.log('Data: ', response.data);
  13. })
  14. .catch(error => {
  15. // Handle error.
  16. console.log('An error occurred:', error.response);
  17. });

添加之后就会返回添加结果了。
image.png

GraphQL 访问 Strapi

除了默认提供的Restful规范的接口,Strapi 也支持使用 GraphQL 来操作数据。
安装 GraphQL:

  1. npm run strapi install graphql

启动应用,访问 http://localhost:1337/graphql

Strapi 数据预取到 Gridsome

Gridsome 从 Strapi 中获取数据,是通过插件@gridsome/source-strapi来实现的。

  1. yarn add @gridsome/source-strapi
  2. npm install @gridsome/source-strapi

配置:

  1. module.exports = {
  2. siteName: 'Gridsome',
  3. plugins: [
  4. {
  5. use: '@gridsome/source-filesystem',
  6. options: {
  7. path: './content/blog/**/*.md',
  8. typeName: 'BlogPost'
  9. }
  10. },
  11. {
  12. use: '@gridsome/source-strapi',
  13. options: {
  14. apiURL: 'http://localhost:1337', // api的基本url
  15. queryLimit: 1000, // 查询条数限制
  16. contentTypes: ['post'], // 想要查询的内容类型(集合类型)
  17. // typeName: 'Strapi', // 会拼接到数据的名称,比如接收到 Post 数据就会把它的名字转成 strapiPost, allStrapiPost
  18. // singleTypes: ['impressum'], // 单一类型
  19. // Possibility to login with a Strapi user,
  20. // when content types are not publicly available (optional).
  21. // 预设置登录信息
  22. //loginData: {
  23. //identifier: 'yanlin',
  24. // password: '123456'
  25. //}
  26. }
  27. }
  28. ]
  29. }

启动应用,打开 http://localhost:8080/___explore,可以看到有一些包含 Strapi 的数据,这些就是从 strapi 获取到的数据。Gridsome 会改变获取到的数据的结构。
Gridsome 获取数据是在预渲染时一次性获取的,之后就算 Strapi 中数据发生变化,也不会去获取。
image.png

博客网站集合设计

作者和文章是一对多的关系,文章和类别是多对多的关系。

Writer

image.png

Article

image.png

Category

image.png

获取文章列表并分页

使用 @paginate 指令和<page-query>接收的 $page: Int 分页。使用 gridsome 自带的页码跳转组件实现翻页。

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header
  5. class="masthead"
  6. style="background-image: url('/img/home-bg.jpg')"
  7. >
  8. <div class="overlay"></div>
  9. <div class="container">
  10. <div class="row">
  11. <div class="col-lg-8 col-md-10 mx-auto">
  12. <div class="site-heading">
  13. <h1>Clean Blog</h1>
  14. <span class="subheading">A Blog Theme by Start Bootstrap</span>
  15. </div>
  16. </div>
  17. </div>
  18. </div>
  19. </header>
  20. <!-- Main Content -->
  21. <div class="container">
  22. <div class="row">
  23. <div class="col-lg-8 col-md-10 mx-auto">
  24. <div class="post-preview" v-for="edge in $page.posts.edges" :key="edge.node.id">
  25. <g-link :to="'/post/' + edge.node.id">
  26. <h2 class="post-title">{{ edge.node.title }}</h2>
  27. </g-link>
  28. <p class="post-meta">
  29. Posted by
  30. <a href="#">{{ edge.node.author.name }}</a>
  31. on {{ edge.node.created_at }}
  32. </p>
  33. <p>
  34. <span v-for="tag in edge.node.categories" :key="tag.id">
  35. <a href="">{{ tag.name }}</a>&nbsp;&nbsp;
  36. </span>
  37. </p>
  38. </div>
  39. <hr />
  40. <!-- Pager -->
  41. <div class="clearfix">
  42. <Pager :info="$page.posts.pageInfo"></Pager>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. </Layout>
  48. </template>
  49. <page-query>
  50. query ($page: Int) {
  51. posts: allStrapiArticle (perPage: 2, page: $page) @paginate {
  52. pageInfo {
  53. perPage,
  54. currentPage,
  55. totalPages,
  56. totalItems,
  57. hasPreviousPage,
  58. hasNextPage,
  59. isFirst,
  60. isLast
  61. }
  62. edges {
  63. node {
  64. id,
  65. title,
  66. created_at,
  67. author {
  68. id,
  69. name
  70. },
  71. categories {
  72. name,
  73. id
  74. }
  75. }
  76. }
  77. }
  78. }
  79. </page-query>
  80. <script>
  81. import { Pager } from 'gridsome'
  82. export default {
  83. metaInfo: {
  84. title: 'Hello, world!'
  85. },
  86. name: 'HomePage',
  87. components: {
  88. Pager
  89. }
  90. }
  91. </script>
  92. <style>
  93. </style>

文章详情页

通过模板页面的方式来配置文章详情页的路由。模板的名称要和单个节点的集合保持一致。

  1. module.exports = {
  2. siteName: 'Gridsome',
  3. plugins: [
  4. {
  5. use: '@gridsome/source-filesystem',
  6. options: {
  7. path: './content/blog/**/*.md',
  8. typeName: 'BlogPost'
  9. }
  10. },
  11. {
  12. use: '@gridsome/source-strapi',
  13. options: {
  14. apiURL: 'http://localhost:1337',
  15. queryLimit: 1000, // Defaults to 100
  16. contentTypes: ['article', 'category'],
  17. // typeName: 'Strapi',
  18. // singleTypes: ['impressum'],
  19. // Possibility to login with a Strapi user,
  20. // when content types are not publicly available (optional).
  21. // loginData: {
  22. // identifier: 'yanlin',
  23. // password: '123456'
  24. // }
  25. }
  26. }
  27. ],
  28. templates: {
  29. // 这个名称和集合名称保持一致
  30. StrapiArticle: [
  31. {
  32. path: '/post/:id',
  33. component: './src/templates/Post.vue'
  34. }
  35. ]
  36. }
  37. }

详情页接收 id 去查询,文章内容用markdown-it渲染成 html 元素。

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header
  5. class="masthead"
  6. :style="{
  7. backgroundImage: `url(http://localhost:1337${$page.post.image.url})`
  8. }"
  9. >
  10. <div class="overlay"></div>
  11. <div class="container">
  12. <div class="row">
  13. <div class="col-lg-8 col-md-10 mx-auto">
  14. <div class="post-heading">
  15. <h1>{{ $page.post.title }}</h1>
  16. <span class="meta">
  17. Posted by
  18. <a href="#">{{ $page.post.author.name }}</a>
  19. {{ $page.post.created_at }}
  20. </span>
  21. </div>
  22. </div>
  23. </div>
  24. </div>
  25. </header>
  26. <!-- Post Content -->
  27. <article>
  28. <div class="container">
  29. <div class="row">
  30. <div class="col-lg-8 col-md-10 mx-auto" v-html="content">
  31. </div>
  32. </div>
  33. </div>
  34. </article>
  35. </Layout>
  36. </template>
  37. <page-query>
  38. query ($id: ID!) {
  39. post: strapiArticle(id: $id) {
  40. id,
  41. title,
  42. content,
  43. created_at,
  44. image {
  45. url
  46. },
  47. author {
  48. name,
  49. id
  50. },
  51. categories{
  52. id,
  53. name
  54. }
  55. }
  56. }
  57. </page-query>
  58. <script>
  59. import MarkDownIt from 'markdown-it'
  60. const md = new MarkDownIt()
  61. export default {
  62. name: 'PostPage',
  63. computed: {
  64. content () {
  65. return md.render(this.$page.post.content)
  66. }
  67. }
  68. }
  69. </script>
  70. <style scoped>
  71. </style>

网站基本信息设置

我们希望网站的基本信息,像介绍和主页图片等,可以不写死在程序里,而是从 GraphQL 中获取到。
Strapi 中可以添加单一类型,它和集合类型的区别是只能添加一条数据。虽然在 Strapi 中单一类型是一种比较特殊的类型,但是 Gridsome 的 GraphQL 获取的时候还是把它和集合类型做同样的处理。
image.png
image.png

我们把主页的信息设置成单一类型,然后获取到它们。

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header
  5. class="masthead"
  6. :style="{backgroundImage: `url(http://localhost:1337${general.shareImage.url})`}"
  7. >
  8. <div class="overlay"></div>
  9. <div class="container">
  10. <div class="row">
  11. <div class="col-lg-8 col-md-10 mx-auto">
  12. <div class="site-heading">
  13. <h1>{{ general.metaTitle }}</h1>
  14. <span class="subheading">{{ general.metaDescription }}</span>
  15. </div>
  16. </div>
  17. </div>
  18. </div>
  19. </header>
  20. <!-- Main Content -->
  21. <div class="container">
  22. <div class="row">
  23. <div class="col-lg-8 col-md-10 mx-auto">
  24. <div class="post-preview" v-for="edge in $page.posts.edges" :key="edge.node.id">
  25. <g-link :to="'/post/' + edge.node.id">
  26. <h2 class="post-title">{{ edge.node.title }}</h2>
  27. <!-- <h3 class="post-subtitle">Problems look mighty small from 150 miles up</h3> -->
  28. </g-link>
  29. <p class="post-meta">
  30. Posted by
  31. <a href="#">{{ edge.node.author.name }}</a>
  32. on {{ edge.node.created_at }}
  33. </p>
  34. <p>
  35. <span v-for="tag in edge.node.categories" :key="tag.id">
  36. <a href="">{{ tag.name }}</a>&nbsp;&nbsp;
  37. </span>
  38. </p>
  39. </div>
  40. <hr />
  41. <!-- Pager -->
  42. <div class="clearfix">
  43. <!-- <a
  44. class="btn btn-primary float-right"
  45. href="#"
  46. >Older Posts &rarr;</a> -->
  47. <Pager :info="$page.posts.pageInfo"></Pager>
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52. </Layout>
  53. </template>
  54. <page-query>
  55. query ($page: Int) {
  56. posts: allStrapiArticle (perPage: 2, page: $page) @paginate {
  57. pageInfo {
  58. perPage,
  59. currentPage,
  60. totalPages,
  61. totalItems,
  62. hasPreviousPage,
  63. hasNextPage,
  64. isFirst,
  65. isLast
  66. }
  67. edges {
  68. node {
  69. id,
  70. title,
  71. created_at,
  72. author {
  73. id,
  74. name
  75. },
  76. categories {
  77. name,
  78. id
  79. }
  80. }
  81. }
  82. }
  83. allStrapiHomePage {
  84. edges {
  85. node {
  86. seo {
  87. metaTitle,
  88. metaDescription,
  89. shareImage {
  90. url
  91. }
  92. }
  93. }
  94. }
  95. }
  96. }
  97. </page-query>
  98. <script>
  99. import { Pager } from 'gridsome'
  100. export default {
  101. metaInfo: {
  102. title: 'Hello, world!'
  103. },
  104. name: 'HomePage',
  105. components: {
  106. Pager
  107. },
  108. computed: {
  109. general () {
  110. return this.$page.allStrapiHomePage.edges[0].node.seo
  111. }
  112. },
  113. }
  114. </script>
  115. <style>
  116. </style>

实现”联系我”

联系页面需要把网站用户填写的信息保存到 Strapi 的集合中。
image.png

新建一个集合contact,字段和上面一样,给 public 角色添加 contact 的create 权限。
image.png

Gridsome 项目中添加 axios 来处理接口。

  1. <template>
  2. <Layout>
  3. <!-- Page Header -->
  4. <header class="masthead" style="background-image: url('/img/contact-bg.jpg')">
  5. <div class="overlay"></div>
  6. <div class="container">
  7. <div class="row">
  8. <div class="col-lg-8 col-md-10 mx-auto">
  9. <div class="page-heading">
  10. <h1>Contact Me</h1>
  11. <span class="subheading">Have questions? I have answers.</span>
  12. </div>
  13. </div>
  14. </div>
  15. </div>
  16. </header>
  17. <!-- Main Content -->
  18. <div class="container">
  19. <div class="row">
  20. <div class="col-lg-8 col-md-10 mx-auto">
  21. <p>Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as possible!</p>
  22. <!-- Contact Form - Enter your email address on line 19 of the mail/contact_me.php file to make this form work. -->
  23. <!-- WARNING: Some web hosts do not allow emails to be sent through forms to common mail hosts like Gmail or Yahoo. It's recommended that you use a private domain email address! -->
  24. <!-- To use the contact form, your site must be on a live web host with PHP! The form will not work locally! -->
  25. <form name="sentMessage" id="contactForm" novalidate @submit.prevent="onSubmit">
  26. <div class="control-group">
  27. <div class="form-group floating-label-form-group controls">
  28. <label>Name</label>
  29. <input v-model="form.name" type="text" class="form-control" placeholder="Name" id="name" required data-validation-required-message="Please enter your name.">
  30. <p class="help-block text-danger"></p>
  31. </div>
  32. </div>
  33. <div class="control-group">
  34. <div class="form-group floating-label-form-group controls">
  35. <label>Email Address</label>
  36. <input v-model="form.email" type="email" class="form-control" placeholder="Email Address" id="email" required data-validation-required-message="Please enter your email address.">
  37. <p class="help-block text-danger"></p>
  38. </div>
  39. </div>
  40. <div class="control-group">
  41. <div class="form-group col-xs-12 floating-label-form-group controls">
  42. <label>Phone Number</label>
  43. <input v-model="form.phone" type="tel" class="form-control" placeholder="Phone Number" id="phone" required data-validation-required-message="Please enter your phone number.">
  44. <p class="help-block text-danger"></p>
  45. </div>
  46. </div>
  47. <div class="control-group">
  48. <div class="form-group floating-label-form-group controls">
  49. <label>Message</label>
  50. <textarea v-model="form.message" rows="5" class="form-control" placeholder="Message" id="message" required data-validation-required-message="Please enter a message."></textarea>
  51. <p class="help-block text-danger"></p>
  52. </div>
  53. </div>
  54. <br>
  55. <div id="success"></div>
  56. <button type="submit" class="btn btn-primary" id="sendMessageButton">Send</button>
  57. </form>
  58. </div>
  59. </div>
  60. </div>
  61. </Layout>
  62. </template>
  63. <script>
  64. import axios from 'axios'
  65. export default {
  66. name: 'ContactPage',
  67. data() {
  68. return {
  69. form: {
  70. name: '',
  71. email: '',
  72. phone: '',
  73. message: '',
  74. }
  75. }
  76. },
  77. methods: {
  78. async onSubmit () {
  79. try {
  80. const { data } = await axios.post('http://localhost:1337/contacts', this.form)
  81. window.alert('发送成功')
  82. } catch (error) {
  83. window.alert('发送失败')
  84. }
  85. }
  86. }
  87. }
  88. </script>

网站部署

Strapi

Strapi 的部署需要使用到数据库,服务器上使用 MySQL,并创建一个数据库 blog
修改 Strapi 项目下的 config 文件夹中的 database.js,配置数据库连接信息:

  1. module.exports = ({ env }) => ({
  2. defaultConnection: 'default',
  3. connections: {
  4. default: {
  5. connector: 'bookshelf',
  6. settings: {
  7. client: 'mysql',
  8. host: env('DATABASE_HOST', 'localhost'),
  9. port: env.int('DATABASE_PORT', 3306),
  10. database: env('DATABASE_NAME', 'blog'),
  11. username: env('DATABASE_USERNAME', 'root'),
  12. password: env('DATABASE_PASSWORD', 'cyl468520253'),
  13. },
  14. options: {},
  15. },
  16. },
  17. })

项目的生产依赖添加mysql连接模块:

  1. {
  2. "name": "blog-strapi",
  3. "private": true,
  4. "version": "0.1.0",
  5. "description": "A Strapi application",
  6. "scripts": {
  7. "develop": "strapi develop",
  8. "start": "strapi start",
  9. "build": "strapi build",
  10. "strapi": "strapi"
  11. },
  12. "devDependencies": {},
  13. "dependencies": {
  14. "mysql": "^2.18.1",
  15. "strapi": "3.6.8",
  16. "strapi-admin": "3.6.8",
  17. "strapi-utils": "3.6.8",
  18. "strapi-plugin-content-type-builder": "3.6.8",
  19. "strapi-plugin-content-manager": "3.6.8",
  20. "strapi-plugin-users-permissions": "3.6.8",
  21. "strapi-plugin-email": "3.6.8",
  22. "strapi-plugin-upload": "3.6.8",
  23. "strapi-plugin-i18n": "3.6.8",
  24. "strapi-connector-bookshelf": "3.6.8",
  25. "knex": "0.21.18",
  26. "sqlite3": "5.0.0",
  27. "mime-types": "^2.1.27",
  28. "strapi-plugin-graphql": "3.6.8"
  29. },
  30. "author": {
  31. "name": "A Strapi developer"
  32. },
  33. "strapi": {
  34. "uuid": "a4e2c290-2fc6-4bdb-bda1-98223db08c34",
  35. "template": "https://github.com/strapi/strapi-template-blog"
  36. },
  37. "engines": {
  38. "node": ">=10.16.0 <=14.x.x",
  39. "npm": "^6.0.0"
  40. },
  41. "license": "MIT"
  42. }

注意:如果本地没有安装 MySQL,那么应该把这个依赖去掉,否则会报错。

推送到远程仓库上。

通过 git 克隆到服务器上,安装依赖,执行 build 再执行 start。

访问服务器的 1337 端口,就能看到熟悉的管理页面。

Strapi 在构建的时候,会自动把集合映射到blog数据库中,生成table
image.png

Gridsome 项目

Gridsome 项目通过 vercel网站部署。
从 GitHub 导入项目之后,点击 Deploy 按钮就能部署网站,而且还会分配域名。

如果要达到修改数据就触发部署的目的,可以在 vercel 使用 Deploy Hooks 生成一个钩子,并在 Strapi 管理页面中的 Webhooks 中添加钩子。

image.png

image.png