Allow guests to create entries from your site’s front end.
Guest Entries requires Craft CMS 4.0.0+ or 5.0.0+.
You can install Guest Entries from the Plugin Store or with Composer.
Go to the Plugin Store in your project’s control panel (in an environment that allows admin changes), search for “Guest Entries,” then click Install.
Open your terminal and run the following commands:
# Navigate to your project directory:
cd /path/to/my-project
# Require the plugin package with Composer:
composer require craftcms/guest-entries -w
# Install the plugin with Craft:
php craft plugin/install guest-entries
From the plugin settings page, you can configure…
- …which sections should allow guest entry submissions;
- …the default entry authors and statuses;
- …and whether submissions should be validated before being accepted.
A basic guest entry template should look something like this:
{# Macro to help output errors: #}
{% macro errorList(errors) %}
{% if errors %}
{{ ul(errors, { class: 'errors' }) }}
{% endif %}
{% endmacro %}
{# Default value for the `entry` variable: #}
{% set entry = entry ?? null %}
<form method="post" action="" accept-charset="UTF-8">
{# Hidden inputs required for the form to work: #}
{{ csrfInput() }}
{{ actionInput('guest-entries/save') }}
{# Custom redirect URI: #}
{{ redirectInput('success') }}
{# Section for new entries: #}
{{ hiddenInput('sectionHandle', 'mySectionHandle') }}
{# Entry properties and custom fields: #}
<label for="title">Title</label>
{{ input('text', 'title', entry ? entry.title, { id: 'title' }) }}
{{ entry ? _self.errorList(entry.getErrors('title')) }}
<label for="body">Body</label>
{{ tag('textarea', {
text: entry ? entry.body,
id: 'body',
name: 'fields[body]',
}) }}
{{ entry ? _self.errorList(entry.getErrors('body')) }}
{# ... #}
<button type="submit">Publish</button>
</form>
Note
The process of submitting data and handling success and error states is outlined in the controller actions documentation.
The following parameters can be sent with a submission:
Name | Notes | Required |
---|---|---|
sectionHandle |
Determines what section the entry will be created in. | ✓ |
sectionUid |
Can be sent in lieu of sectionHandle . |
|
sectionId |
Can be sent in lieu of sectionHandle . |
|
typeId |
Entry type ID to use. This may affect which custom fields are required. When absent, the first configured type for the specified section is used. | |
title |
Optional if the section has automatic title formatting enabled. | ✓ |
slug |
Explicitly sets the new entry’s slug. | |
postDate |
Value should be processable by DateTimeHelper::toDateTime() |
|
expiryDate |
Value should be processable by DateTimeHelper::toDateTime() |
|
parentId |
Nest this entry under another. Invalid for channels and structures with a maximum depth of 1 . |
|
siteId |
Create the entry in a specific site. | |
enabledForSite |
Whether the entry should be enabled in this site. The global enabled setting is configurable by administrators, so this alone will not immediately publish something. |
|
fields[...] |
Any custom fields you want guests to be able to populate. |
The plugin determines what section the new entry is created in by looking for a sectionHandle
, sectionUid
, or sectionId
param, in this order. Entry types, on the other hand, can only be defined by a typeId
param—but because IDs can be unstable between environments, you must look them up by a known identifier.
Granted you will already have a section (or at least a section handle), the easiest way to do this is via the section model:
{% set targetSection = craft.app.sections.getSectionByHandle('resources') %}
{% set entryTypes = targetSection.getEntryTypes() %}
{# Select a single type, identified by its handle: #}
{% set targetEntryType = collect(entryTypes).firstWhere('handle', 'document') %}
{{ hiddenInput('sectionId', targetSection.id) }}
{{ hiddenInput('typeId', targetEntryType.id) }}
Custom field data should be nested under the fields
key, with the field name in [squareBrackets]
:
<input
type="text"
name="fields[myCustomFieldHandle]"
value="{{ entry ? entry.myCustomFieldHandle }}">
If entries in the designated section are enabled by default, validation will occur on all custom fields, meaning those marked as required in the entry type’s field layout must be sent with the submission. Refer to the field types documentation to learn about the kinds of values that Craft accepts.
Warning
Omitting a field from your form does not mean it is safe from tampering! Clever users may be able to modify the request payload and submit additional field data. If this presents a problem for your site, consider using an event to clear values or reject submissions.
If there are validation errors on the entry, the page will be reloaded with the populated craft\elements\Entry
object available under an entry
variable. You can access the posted values from that object as though it were a normal entry—or display errors with getErrors()
, getFirstError()
, or getFirstErrors()
.
Note
Theentry
variable can be renamed with the “Entry Variable Name” setting in the control panel. This might be necessary if you want to use a form on an entry page that already injects a variable of that name.
Send a redirect
param to send the user to a specific location upon successfully saving an entry. In the example above, this is handled via the redirectInput('...')
function. The path is evaluated as an object template, and can include properties of the saved entry in {curlyBraces}
.
If you submit your form via Ajax with an Accept: application/json
header, a JSON response will be returned with the following keys:
success
(boolean) – Whether the entry was saved successfullyerrors
(object) – All of the validation errors indexed by field name (if not saved)id
(string) – the entry’s ID (if saved)title
(string) – the entry’s title (if saved)authorUsername
(string) – the entry’s author’s username (if saved)dateCreated
(string) – the entry’s creation date in ISO 8601 format (if saved)dateUpdated
(string) – the entry’s update date in ISO 8601 format (if saved)postDate
(string, null) – the entry’s post date in ISO 8601 format (if saved and enabled)url
(string, null) – the entry’s public URL (if saved, enabled, and in a section that has URLs)
Using a redirect
param allows you to show a user some or all of the content they just submitted—even if the entry is disabled, by default.
Warning
Take great care when displaying untrusted content on your site, especially when subverting moderation processes!
Entries in sections with URLs can be viewed immediately, with this redirect
param:
{{ redirectInput('{url}') }}
In order to display an entry that is disabled, you will need to set up a custom route…
<?php
return [
// This route uses the special `{uid}` token, which will
// match any UUIDv4 generated by Craft:
'submissions/confirmation/<entryUid:{uid}>' => ['template' => '_submissions/confirmation'],
];
…and direct users to it by including {{ redirectInput('submissions/confirmation/{uid}') }}
in the entry form. Your template (_submissions/confirmation.twig
) will be responsible for looking up the disabled entry and displaying it, based on the entryUid
route token that Craft makes available:
{% set preview = craft.entries()
.status('disabled')
.section('documents')
.uid(entryUid)
.one() %}
{# Bail if it doesn’t exist: #}
{% if not preview %}
{% exit 404 %}
{% endif %}
{# Supposing the user’s name was recorded in the `title` field: #}
<h1>Thanks, {{ preview.title }}!</h1>
<p>Your submission has been recorded, and is awaiting moderation.</p>
This query selects only disabled
entries so that the “preview” is invalidated once the entry goes live. This “confirmation” URI does not need to match the actual URI of the entry.
Guest Entries augments the normal events emitted during the entry lifecycle with a few of its own, allowing developers to customize the submission process.
The following snippets should be added to your plugin or module’s init()
method, per the official event usage instructions.
Plugins can be notified before a guest entry is saved, using the beforeSaveEntry
event. This is also an opportunity to flag the submission as spam, and prevent it being saved:
use craft\helpers\StringHelper;
use craft\guestentries\controllers\SaveController;
use craft\guestentries\events\SaveEvent;
use yii\base\Event;
// ...
Event::on(
SaveController::class,
SaveController::EVENT_BEFORE_SAVE_ENTRY,
function(SaveEvent $e) {
// Get a reference to the entry object:
$entry = $e->entry;
// Perform spam detection logic of your own design:
if (StringHelper::contains($entry->title, 'synergy', false)) {
// Set the event property:
$e->isSpam = true;
}
}
);
Plugins can be notified after a guest entry is saved, using the afterSaveEntry
event:
use craft\guestentries\controllers\SaveController;
use craft\guestentries\events\SaveEvent;
use yii\base\Event;
// ...
Event::on(
SaveController::class,
SaveController::EVENT_AFTER_SAVE_ENTRY,
function(SaveEvent $e) {
// Grab the entry
$entry = $e->entry;
// Was it flagged as spam?
$isSpam = $e->isSpam;
}
);
Plugins can be notified right after a submission is determined to be invalid using the afterError
event:
use craft\guestentries\controllers\SaveController;
use craft\guestentries\events\SaveEvent;
use yii\base\Event;
// ...
Event::on(
SaveController::class,
SaveController::EVENT_AFTER_ERROR,
function(SaveEvent $e) {
// Grab the entry
$entry = $e->entry;
// Get any validation errors
$errors = $entry->getErrors();
}
);