KurocoとNuxt.jsで、コンテンツにコメント機能を追加する
Kurocoを利用したNuxt.jsプロジェクトで、アクティビティ:コメント機能の実装方法を紹介します。
今回は例として、下記流れにて処理を実装します。
- API・エンドポイントの作成
- ニュース詳細画面にコメント機能を追加
事前準備
Nuxt.jsプロジェクトの作成について
このページは、KurocoとNuxt.jsでのプロジェクトが構築済み、
かつ既に何らかの記事が閲覧できること、またprofile
エンドポイントが有効であることを前提としています。
まだNuxt.jsプロジェクトを構築していない場合、チュートリアル ->KurocoとNuxt.jsで、コンテンツ一覧ページを作成するを参照してください。
またprofile
などはチュートリアル ->KurocoとNuxt.jsで、ログイン画面を構築するを参考にしてください。
今回はcookieによるログイン制御を前提とします。
作成済みの「お知らせ詳細」のエンドポイントをコピー、未ログインメンバーによるコメントの許可設定をした後、お知らせ詳細とそれに紐づくコメントのフォームを作成します。
APIの作成
未承認ユーザーからの操作を許可するために、新しくAPIを作成します。
Kuroco管理画面のAPIより「追加」をクリックします。
API作成画面が表示されます。
下記入力し「追加する」をクリックします。
項目 | 設定内容 |
---|---|
タイトル | Comment Test |
版 | 1.0 |
説明 | コメント機能の確認用API |
CORSの設定
次にCORSの設定をします。[CORSを設定する] をクリックします。
CORS_ALLOW_ORIGINSの [Add Origin] をクリックし、下記を追加します。
- http://localhost:3000
CORS_ALLOW_METHODSの [Add Method] をクリックし、下記を追加します。
- GET
- POST
- OPTIONS
設定できたら[保存する]をクリックし、CORSの設定が完了です。
ログイン不要でコメント機能の動作を確認する
アクティビティ定義の作成
未承認ユーザーからのコメント操作を許可するために、アクティビティ定義を編集します。
[アクティビティ定義]をクリックします。
[追加する]をクリックします。
APIリクエスト制限、投稿制限を
閲覧可
、即公開
として[追加する]をクリックします。
後ほど利用するので、作成したアクティビティIDをメモしておきます。
エンドポイントの作成
エンドポイントを作成します。今回は下記エンドポイントを作成します。
- newsdetailエンドポイント -> ニュース詳細
- commentsエンドポイント -> コメントの取得用
- commentエンドポイント -> コメントの追加用
- comment_deleteエンドポイント -> コメントの削除用
Comment TestのAPIで「新しいエンドポイントの追加」をクリックし、それぞれ作成します。
newsdetailエンドポイントの作成
newsdetailエンドポイントを下記設定にて作成します。
項目 | 設定内容 |
---|---|
パス | newsdetail |
カテゴリー | コンテンツ |
モデル | Topics v1 |
オペレーション | details |
APIリクエスト制限 | None |
topics_group_id | 表示するコンテンツ定義ID(9) |
設定完了後、「追加する」をクリックしnewsdetailエンドポイント完成です。
commentsエンドポイントの作成
commentsエンドポイントを下記設定にて作成します。
項目 | 設定内容 |
---|---|
パス | comments |
カテゴリー | アクティビティ |
モデル | Comment v1 |
オペレーション | list |
APIリクエスト制限 | None |
id | アクティビティID(37) |
module_type | topics |
new_order_flg | チェックを入れる |
設定完了後、「追加する」をクリックしcommentsエンドポイント完成です。
commentエンドポイントの作成
commentエンドポイントを下記設定にて作成します。
項目 | 設定内容 |
---|---|
パス | comment |
カテゴリー | アクティビティ |
モデル | Comment v1 |
オペレーション | insert |
APIリクエスト制限 | None |
id | アクティビティID(37) |
設定完了後、「追加する」をクリックしcommentエンドポイント完成です。
comment_deleteエンドポイントの作成
comment_deleteエンドポイントを下記設定にて作成します。
項目 | 設定内容 |
---|---|
パス | comment_delete |
カテゴリー | アクティビティ |
モデル | Comment v1 |
オペレーション | delete |
APIリクエスト制限 | None |
id | アクティビティID(37) |
設定完了後、「追加する」をクリックしcomment_deleteエンドポイント完成です。
今回の例では、コメントの追加/削除するエンドポイントはセキュリティNone
にしていますが、実際のサイトではコメントの追加/削除を許可するグループを設定してください。
フロントエンドの設定
コメント機能付きニュース詳細ページの追加
今回は既存のニュース詳細画面を参考に画面を作成し、同ページにコメント機能を追加します。
まずは未承認のユーザーでもコメントの閲覧/投稿がきるようにし動作の確認をします。
画面を表示する際にニュースへ紐づく全てのコメントを取得し、表示します。
ユーザー名入力欄とコメント投稿のフォームが存在し、ユーザー名入力のあと、追加したコメントは即時に画面へ反映されます。
またコメントそれぞれには削除ボタンが存在します。
コメントの削除はコメントの投稿・削除にパスワード(delkey)を使用するように構築するか、ログインを前提として、コメントを投稿した本人でないとできません。
この段階では削除ボタンをクリックした場合、Code 422 "パスワードが一致しないか、書き込んだ本人でないため削除できません。" のエラーが発生します。
pages/news/test_with_comment.vue
を追加します。
<template>
<div>
<h1 class="title">{{ response.details.subject }}</h1>
<div class="post" v-html="response.details.contents"></div>
<p v-if="resultMessage !== null">
{{ resultMessage }}
</p>
<div>
please type your name: <input v-model="userName" type="text" placeholder="your name">
</div>
<div>
<ul v-for="comment in comments" :key="comment.comment_id">
<li>
{{ comment.note }} by {{ comment.name }}
<button type="button" @click="() => deleteComment(comment.comment_id)">
delete
</button>
</li>
</ul>
<form @submit.prevent="submitComment">
<input v-model="inputComment" type="text" placeholder="comment">
<button type="submit" :disabled="inputComment === '' || userName === ''">
submit
</button>
</form>
</div>
</div>
</template>
<script>
async function getAllComments (topics_id) {
const { list } = await this.$axios.$get(
'/rcms-api/21/comments',
{
params: {
module_id: topics_id,
cnt: 999
}
}
)
return list
}
export default {
async asyncData({ $axios, params }) {
try {
const response = await $axios.$get(
'/rcms-api/21/newsdetail/1047'
)
return { response, comments: await getAllComments.call({ $axios }, response.details.topics_id) }
} catch (e) {
console.log(e.message)
}
},
data () {
return {
userName: '',
response: null,
comments: [],
inputComment: '',
resultMessage: null,
}
},
methods: {
async submitComment () {
await this.$axios.$post('/rcms-api/21/comment', {
module_id: this.response.details.topics_id,
name: this.userName,
note: this.inputComment
})
this.comments = await getAllComments.call(this, this.response.details.topics_id)
this.inputComment = ''
},
async deleteComment (commentId) {
try{
await this.$axios.$post(`/rcms-api/21/comment_delete/${commentId}`, {})
this.comments = await getAllComments.call(this, this.response.details.topics_id)
this.inputComment = ''
} catch (error) {
this.resultMessage = error.response.data.errors[0].message
}
}
}
}
</script>
以下のようにコメントの追加ができることを確認します。
コメントを削除可能にする(delkeyの使用)
未承認のユーザーでもコメントの削除ができるようにするため、delkey
を使用して削除をリクエストするように変更します。
delkey
はコメントを追加する際に付与できる任意の値です。
今回は以下の仕様で実装します。
- コメントを追加する段階で
delkey
を自動的に付与 delkey
はブラウザのローカルストレージに保存- 削除時にブラウザのローカルストレージから
delkey
を呼び出す
ブラウザへの保存はローカルストレージを使用するため、ブラウザの変更や異なる端末で操作再開する場合には削除できない点にご注意ください。
pages/news/test_with_comment.vue
を下記のように変更します。
@@ -1,85 +1,106 @@
<template>
<div>
<h1 class="title">{{ response.details.subject }}</h1>
<div class="post" v-html="response.details.contents"></div>
<p v-if="resultMessage !== null">
{{ resultMessage }}
</p>
<div>
please type your name: <input v-model="userName" type="text" placeholder="your name">
</div>
<div>
<ul v-for="comment in comments" :key="comment.comment_id">
<li>
{{ comment.note }} by {{ comment.name }}
- <button type="button" @click="() => deleteComment(comment.comment_id)">
+ <button v-if="commentHistory.map(({ id }) => id).includes(comment.comment_id)" type="button" @click="() => deleteComment(comment.comment_id)">
delete
</button>
</li>
</ul>
<form @submit.prevent="submitComment">
<input v-model="inputComment" type="text" placeholder="comment">
<button type="submit" :disabled="inputComment === '' || userName === ''">
submit
</button>
</form>
</div>
</div>
</template>
<script>
async function getAllComments (topics_id) {
const { list } = await this.$axios.$get(
'/rcms-api/21/comments',
{
params: {
module_id: topics_id,
cnt: 999
}
}
)
return list
}
+const COMMENT_HISTORY_KEY = 'CommentHistory'
+
export default {
async asyncData({ $axios, params }) {
try {
const response = await $axios.$get(
'/rcms-api/21/newsdetail/1047'
)
return { response, comments: await getAllComments.call({ $axios }, response.details.topics_id) }
} catch (e) {
console.log(e.message)
}
},
data () {
return {
userName: '',
response: null,
comments: [],
inputComment: '',
+ commentHistory: [],
resultMessage: null,
}
},
+ mounted () {
+ this.commentHistory = JSON.parse(localStorage.getItem(COMMENT_HISTORY_KEY)) || []
+ },
methods: {
async submitComment () {
- await this.$axios.$post('/rcms-api/21/comment', {
+ const delkey = `${this.userName}_${Date.now()}`
+ const submitResponse = await this.$axios.$post('/rcms-api/21/comment', {
module_id: this.response.details.topics_id,
name: this.userName,
- note: this.inputComment
+ note: this.inputComment,
+ delkey
})
+ this.addCommentHistory({ id: submitResponse.id, delkey })
this.comments = await getAllComments.call(this, this.response.details.topics_id)
this.inputComment = ''
},
async deleteComment (commentId) {
try{
- await this.$axios.$post(`/rcms-api/21/comment_delete/${commentId}`, {})
+ await this.$axios.$post(`/rcms-api/21/comment_delete/${commentId}`, {
+ delkey: this.commentHistory.find(({ id }) => `${id}` === `${commentId}`).delkey
+ })
+ this.deleteCommentHistory(commentId)
this.comments = await getAllComments.call(this, this.response.details.topics_id)
this.inputComment = ''
} catch (error) {
this.resultMessage = error.response.data.errors[0].message
}
+ },
+ addCommentHistory (payload) {
+ const restored = JSON.parse(localStorage.getItem(COMMENT_HISTORY_KEY)) || []
+ restored.push(payload)
+ localStorage.setItem(COMMENT_HISTORY_KEY, JSON.stringify(restored))
+ this.commentHistory = restored
+ },
+ deleteCommentHistory (commentId) {
+ const restored = JSON.parse(localStorage.getItem(COMMENT_HISTORY_KEY)) || []
+ const filtered = restored.filter(({ id }) => `${id}` !== `${commentId}`)
+ localStorage.setItem(COMMENT_HISTORY_KEY, JSON.stringify(filtered))
+ this.commentHistory = filtered
}
}
}
</script>
以下のようにコメント済みのもののみ削除ボタンが表示して削除ができることを確認します。
コメントをログイン必須にする
コメント機能のような変更/削除のためのPOSTエンドポイントを無施策に開放してしまうと、DoS攻撃(同時に大量のデータをPOSTすることで、DBをパンクさせるサイバー攻撃)に弱くなります。
Kurocoのエンドポイントでは、同じIPアドレスから短時間に何回もコメントをされた場合に、投稿を受け付けないエラーを返す機能を持っていますが、本チュートリアルではページの閲覧、コメントの表示・投稿・削除にログインを必須とさせるように実装します。
アクティビティ定義の更新
[アクティビティ定義]をクリックします。
作成したアクティビティのタイトルをクリックします。
未ログインメンバーのAPIリクエスト制限、投稿制限を
閲覧不可
、受け付けない
として[更新する]をクリックします。
フロントエンドの更新
pages/news/test_with_comment.vue
を次のように修正します。
<template>
<div>
<h1 class="title">{{ response.details.subject }}</h1>
<div class="post" v-html="response.details.contents"></div>
<div>
<p v-if="resultMessage !== null">
{{ resultMessage }}
</p>
<ul v-for="comment in comments" :key="comment.comment_id">
<li>
{{ comment.note }} by {{ comment.name }}
<button type="button" @click="() => deleteComment(comment.comment_id)">
delete
</button>
</li>
</ul>
<form @submit.prevent="submitComment">
<input v-model="inputComment" type="text" placeholder="comment">
<button type="submit" :disabled="inputComment === ''">
submit
</button>
</form>
</div>
</div>
</template>
<script>
async function getAllComments (topics_id) {
const { list } = await this.$axios.$get(
'/rcms-api/21/comments',
{
params: {
module_id: topics_id,
cnt: 999
}
}
)
return list
}
import { mapActions } from 'vuex'
export default {
middleware: 'auth',
async asyncData ({ $axios, params }) {
try {
const profile = await $axios.$get('/rcms-api/18/profile')
const response = await $axios.$get(
'/rcms-api/21/newsdetail/1047'
)
return { profile, response, comments: await getAllComments.call({ $axios }, response.details.topics_id) }
} catch (e) {
console.log(e.message)
}
},
data () {
return {
userName: '',
response: null,
comments: [],
inputComment: '',
resultMessage: null,
}
},
methods: {
async submitComment () {
await this.$axios.$post('/rcms-api/21/comment', {
module_id: this.response.details.topics_id,
name: `${this.profile.name1} ${this.profile.name2}`,
mail: this.profile.email,
note: this.inputComment
})
this.comments = await getAllComments.call(this, this.response.details.topics_id)
this.inputComment = ''
},
async deleteComment (commentId) {
try{
await this.$axios.$post(`/rcms-api/21/comment_delete/${commentId}`, {})
this.comments = await getAllComments.call(this, this.response.details.topics_id)
this.inputComment = ''
} catch (error) {
this.resultMessage = error.response.data.errors[0].message
}
}
}
}
</script>
最後に動作確認をして完了です。
サポート
お探しのページは見つかりましたか?解決しない場合は、問い合わせフォームからお問い合わせいただくか、Slackコミュニティにご参加ください。