Comments
Let readers join the conversation with moderated blog comments.
A blog feels alive when readers can respond to your posts. In this step, you will add a comment form to the detail page using a submission blueprint, a special Tailor blueprint type designed for accepting user generated content. Comments arrive with a pending status and only appear on the site after you approve them in the backend.
# Creating the Comment Blueprint
Create a new file at themes/mytheme/blueprints/blog/comment.yaml:
handle: Blog\Comment
type: submission
name: Comment
submission:
titleTemplate: '{{ str_limit(content, 60) }}'
navigation:
label: Comments
parent: Blog\Post
icon: ph ph-chat-circle
order: 400
fields:
author_name:
label: Name
type: text
validation: required|min:2|max:100
author_email:
label: Email Address
type: email
validation: required|email
content:
label: Comment
type: textarea
validation: required|min:5|max:2000
post:
label: Post
type: entries
source: Blog\Post
maxItems: 1
# What This Does
type: submission: a blueprint type made for user generated content. Records are created from the frontend, arrive as Pending, and stay hidden from visitors until approved.validation: standard validation rules applied when the form is submitted. Invalid input is rejected with an error message against the field.titleTemplate: builds the record title shown in the backend list from the first 60 characters of the comment.post: an entries field that links each comment to the post it was written on, just like the categories and tags fields on the post blueprint.
Every field defined on a submission blueprint accepts input from the frontend. Only define fields that visitors are allowed to populate, and keep internal fields like moderator notes out of the blueprint.
Run the migration to create the table:
php artisan tailor:migrate
# Adding the Components
The detail page needs two components: a Collection to read the approved comments and a Submission to handle the form.
- Navigate to Editor → Pages and open the Blog Post page.
- Switch to the code editor view and add the following to the page's configuration section (the INI block at the top of the file):
[collection comments]
handle = "Blog\Comment"
[submission commentForm]
handle = "Blog\Comment"
Both components use the same Blog\Comment handle but do different jobs. The names comments and commentForm are component aliases, which let you refer to each one separately in the markup.
# Displaying Comments
Still on the Blog Post page, add the comment list to the markup, just before the closing </article> tag:
<div class="mt-8 pt-6 border-t border-gray-200">
{% set postComments = comments.where('post_id', section.id).orderBy('created_at', 'asc').get() %}
<h2 class="text-2xl font-semibold mb-6">
{{ postComments.count }} {{ postComments.count == 1 ? 'comment' : 'comments' }}
</h2>
{% for comment in postComments %}
<div class="mb-6 {{ not loop.last ? 'pb-6 border-b border-gray-100' }}">
<div class="text-sm mb-2">
<span class="font-medium">
{{ comment.author_name }}
</span>
<span class="text-gray-500 ml-2">
{{ comment.created_at|date('F j, Y') }}
</span>
</div>
<p class="text-gray-700">
{{ comment.content|nl2br }}
</p>
</div>
{% else %}
<p class="text-gray-500">
Be the first to share your thoughts on this post.
</p>
{% endfor %}
<div class="mt-8">
<h3 class="text-xl font-semibold mb-4">
Leave a Comment
</h3>
{% ajaxPartial 'comment-form' %}
</div>
</div>
Click Save. The page will show an error until the partial exists, so create that next.
# How This Works
comments.where('post_id', section.id): the Collection component returns a query you can refine. This filters comments to the current post using the relation column created by thepostfield, ordered oldest first like a conversation.comment.content|nl2br: escapes the comment text and converts line breaks to<br>tags, so multi-paragraph comments display correctly and any HTML a visitor types is rendered harmless.{% ajaxPartial %}: like{% partial %}, but the partial can refresh itself after an AJAX request. The comment form uses this to swap itself for a thank you message.
# Creating the Comment Form
- Navigate to Editor → Partials and click + Add.
- Set the File Name to
comment-form. - Enter the following markup:
{% if formSubmitted %}
<div class="p-4 bg-green-50 text-green-700 rounded-md">
Thank you for your comment! It will appear here once it has been approved.
</div>
{% else %}
<form
data-request="commentForm::onFormSubmit"
data-request-update="{ _self: true }"
data-request-flash
>
<input type="hidden" name="post" value="{{ section.id }}">
<div class="grid md:grid-cols-2 gap-4 mb-4">
<div>
<label for="commentName" class="block text-sm font-medium mb-1">
Name
</label>
<input
type="text"
name="author_name"
id="commentName"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
required
>
</div>
<div>
<label for="commentEmail" class="block text-sm font-medium mb-1">
Email address
</label>
<input
type="email"
name="author_email"
id="commentEmail"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
required
>
</div>
</div>
<div class="mb-4">
<label for="commentContent" class="block text-sm font-medium mb-1">
Comment
</label>
<textarea
name="content"
id="commentContent"
rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
required
></textarea>
</div>
<div style="display:none">
<input type="text" name="_oc_hp" value="" autocomplete="off" tabindex="-1">
</div>
<button
type="submit"
data-attach-loading
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
>
Post Comment
</button>
</form>
{% endif %}
- Click Save.
# How This Works
commentForm::onFormSubmit: calls theonFormSubmithandler on the Submission component, using thecommentForm::alias prefix you defined in the INI section. The handler validates the input against the blueprint rules and saves a pending comment.data-request-update="{ _self: true }": after a successful submission, the framework re-renders this partial. TheformSubmittedvariable is nowtrue, so the visitor sees the thank you message instead of the form.name="post"hidden field: fills thepostfield on the blueprint, linking the comment to the post being viewed._oc_hphidden field: a honeypot for spam protection. Humans never see it, but bots fill in every field, and any submission with a value here is blocked. The component also rate limits submissions to 6 per minute for each IP address.data-request-flash: shows validation errors as flash messages, using the setup from the Quick Start.
# Moderating Comments
New comments do not appear on the site until you approve them.
- Preview your site, open a blog post, and submit a test comment. The thank you message appears, but the comment is not listed yet.
- In the backend, navigate to Blog → Comments. Your comment is listed with a Pending status.
- Tick the checkbox next to the comment. Approve, Reject, and Spam buttons appear above the list.
- Click Approve, then refresh the blog post on the frontend. The comment is now visible.
The three actions cover the moderation workflow:
- Approve: makes the comment visible on the frontend.
- Reject: hides the comment. Rejected comments are kept for 30 days and then deleted forever.
- Spam: rejects the comment and also rejects any other pending submissions from the same IP address, cleaning up bot floods in one click.
Use the status filter above the list to switch between Pending, Approved, and Rejected comments, so a busy comments section stays manageable.
# Try It Out
- Submit a comment with a one-character name. The form shows a validation error, enforced by the
min:2rule on the blueprint. - Submit a valid comment and approve it in the backend. It appears on the post, oldest first.
- Submit a comment on a different post and confirm it only appears on that post after approval.
- Try rejecting a comment and watch it disappear from the pending list into the Rejected filter.
# Next Steps
Continue to RSS Feed to add an RSS feed so readers can subscribe to your blog.
For the full reference, see Submission Component and Submission Blueprint in the developer documentation.