Skip to content

Commit

Permalink
Custom SEO (#154)
Browse files Browse the repository at this point in the history
* Enable Pro plan - WIP

* no pricing page if have no paid plans

* Set pricing ids in env

* views & submissions FREE for all

* extra param for env

* form password FREE for all

* Custom Code is PRO feature

* Replace codeinput prism with codemirror

* Better form Cleaning message

* Added risky user email spam protection

* fix form cleaning

* Custom SEO

* fix custom seo formcleaner

* remvoe fix condition
  • Loading branch information
formsdev authored Aug 30, 2023
1 parent fb79a5b commit 01a01a8
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 13 deletions.
3 changes: 3 additions & 0 deletions app/Http/Requests/UserFormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ public function rules()
// Security & Privacy
'can_be_indexed' => 'boolean',
'password' => 'sometimes|nullable',

// Custom SEO
'seo_meta' => 'nullable|array'
];
}

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Resources/FormResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public function toArray($request)
'slack_webhook_url' => $this->slack_webhook_url,
'discord_webhook_url' => $this->discord_webhook_url,
'removed_properties' => $this->removed_properties,
'last_edited_human' => $this->updated_at?->diffForHumans()
'last_edited_human' => $this->updated_at?->diffForHumans(),
'seo_meta' => $this->seo_meta
] : [];

$baseData = $this->getFilteredFormData(parent::toArray($request), $this->userIsFormOwner());
Expand Down
8 changes: 6 additions & 2 deletions app/Models/Forms/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,19 @@ class Form extends Model

// Security & Privacy
'can_be_indexed',
'password'
'password',

// Custom SEO
'seo_meta'
];

protected $casts = [
'properties' => 'array',
'database_fields_update' => 'array',
'closes_at' => 'datetime',
'tags' => 'array',
'removed_properties' => 'array'
'removed_properties' => 'array',
'seo_meta' => 'object'
];

protected $appends = [
Expand Down
24 changes: 24 additions & 0 deletions app/Service/Forms/FormCleaner.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class FormCleaner

private array $data;

// For remove keys those have empty value
private array $customKeys = ['seo_meta'];

private array $formDefaults = [
'notifies' => false,
'no_branding' => false,
Expand All @@ -32,6 +35,7 @@ class FormCleaner
'discord_webhook_url' => null,
'editable_submissions' => false,
'custom_code' => null,
'seo_meta' => []
];

private array $fieldDefaults = [
Expand All @@ -49,6 +53,7 @@ class FormCleaner
'discord_webhook_url' => "Discord webhook disabled.",
'editable_submissions' => 'Users will not be able to edit their submissions.',
'custom_code' => 'Custom code was disabled',
'seo_meta' => 'Custom code was disabled',

// For fields
'file_upload' => "Link field is not a file upload.",
Expand Down Expand Up @@ -202,6 +207,9 @@ private function clean(array &$data, array $defaults, $simulation = false): void
// Get value from form
$formVal = Arr::get($data, $key);

// Transform customkeys values
$formVal = $this->cleanCustomKeys($key, $formVal);

// Transform boolean values
$formVal = (($formVal === 0 || $formVal === "0") ? false : $formVal);
$formVal = (($formVal === 1 || $formVal === "1") ? true : $formVal);
Expand Down Expand Up @@ -242,4 +250,20 @@ private function cleanField(array &$data, array $defaults, $simulation = false):
}*/
}

// Remove keys those have empty value
private function cleanCustomKeys($key, $formVal)
{
if (in_array($key, $this->customKeys) && $formVal !== null) {
$newVal = [];
foreach ($formVal as $k => $val) {
if ($val) {
$newVal[$k] = $val;
}
}
return $newVal;
}

return $formVal;
}

}
20 changes: 15 additions & 5 deletions app/Service/SeoMetaResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,25 @@ private function getFormShowMeta(): array
{
$form = Form::whereSlug($this->patternData['slug'])->firstOrFail();

$meta = [
'title' => $form->title . $this->titleSuffix(),
];
if($form->description){
$meta = [];
if ($form->is_pro && $form->seo_meta->page_title) {
$meta['title'] = $form->seo_meta->page_title;
} else {
$meta['title'] = $form->title . $this->titleSuffix();
}

if ($form->is_pro && $form->seo_meta->page_description) {
$meta['description'] = $form->seo_meta->page_description;
} else if ($form->description) {
$meta['description'] = Str::of($form->description)->limit(160);
}
if($form->cover_picture){

if ($form->is_pro && $form->seo_meta->page_thumbnail) {
$meta['image'] = $form->seo_meta->page_thumbnail;
} else if ($form->cover_picture) {
$meta['image'] = $form->cover_picture;
}

return $meta;
}

Expand Down
3 changes: 2 additions & 1 deletion database/factories/FormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ public function definition()
'tags' => [],
'slack_webhook_url' => null,
'editable_submissions_button_text' => 'Edit submission',
'confetti_on_submission' => false
'confetti_on_submission' => false,
'seo_meta' => [],
];
}

Expand Down
32 changes: 32 additions & 0 deletions database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('forms', function (Blueprint $table) {
$table->json('seo_meta')->default('{}');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('forms', function (Blueprint $table) {
$table->dropColumn('seo_meta');
});
}
};
2 changes: 1 addition & 1 deletion resources/js/components/open/forms/OpenForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ export default {
const formData = clonedeep(this.dataForm ? this.dataForm.data() : {})
let urlPrefill = null
if (this.isPublicFormPage && this.form.is_pro) {
if (this.isPublicFormPage) {
urlPrefill = new URLSearchParams(window.location.search)
}
Expand Down
5 changes: 4 additions & 1 deletion resources/js/components/open/forms/components/FormEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<form-about-submission/>
<form-notifications/>
<form-security-privacy/>
<form-custom-seo />
<form-custom-code/>
<form-integrations/>
</div>
Expand Down Expand Up @@ -66,6 +67,7 @@ import FormNotifications from './form-components/FormNotifications.vue'
import FormIntegrations from './form-components/FormIntegrations.vue'
import FormEditorPreview from './form-components/FormEditorPreview.vue'
import FormSecurityPrivacy from './form-components/FormSecurityPrivacy.vue'
import FormCustomSeo from './form-components/FormCustomSeo.vue'
import saveUpdateAlert from '../../../../mixins/forms/saveUpdateAlert.js'
export default {
Expand All @@ -80,7 +82,8 @@ export default {
FormStructure,
FormInformation,
FormErrorModal,
FormSecurityPrivacy
FormSecurityPrivacy,
FormCustomSeo
},
mixins: [saveUpdateAlert],
props: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<collapse class="p-5 w-full border-b" :default-value="false">
<template #title>
<h3 id="v-step-2" class="font-semibold text-lg">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="h-5 w-5 inline text-gray-500 mr-2 -mt-1"
>
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>

Link Settings - SEO
<pro-tag />
</h3>
</template>
<p class="mt-4 text-gray-500 text-sm">
Customize the image and text that appear when you share your form on other sites (Open Graph).
</p>
<text-input v-model="form.seo_meta.page_title" name="page_title" class="mt-4"
label="Page Title" help="Under or approximately 60 characters"
/>
<text-area-input v-model="form.seo_meta.page_description" name="page_description" class="mt-4"
label="Page Description" help="Between 150 and 160 characters"
/>
<image-input v-model="form.seo_meta.page_thumbnail" name="page_thumbnail" class="mt-4"
label="Page Thumbnail Image" help="Also know as og:image - 1200 X 630"
/>
</collapse>
</template>

<script>
import Collapse from '../../../../common/Collapse.vue'
import ProTag from '../../../../common/ProTag.vue'
export default {
components: { Collapse, ProTag },
props: {},
data () {
return {}
},
computed: {
form: {
get () {
return this.$store.state['open/working_form'].content
},
/* We add a setter */
set (value) {
this.$store.commit('open/working_form/set', value)
}
}
},
watch: {},
mounted () {
['page_title', 'page_description', 'page_thumbnail'].forEach((keyname) => {
if (this.form.seo_meta[keyname] === undefined) {
this.form.seo_meta[keyname] = null
}
})
},
methods: {}
}
</script>
5 changes: 4 additions & 1 deletion resources/js/mixins/form_editor/initForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export default {
confetti_on_submission: false,

// Security & Privacy
can_be_indexed: true
can_be_indexed: true,

// Custom SEO
seo_meta: {}
})
},
}
Expand Down
3 changes: 2 additions & 1 deletion resources/js/mixins/seo-meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ export default {
const title = this.metaTitle ?? 'OpnForm'
const description = this.metaDescription ?? "Create beautiful forms for free. Unlimited fields, unlimited submissions. It's free and it takes less than 1 minute to create your first form."
const image = this.metaImage ?? this.asset('img/social-preview.jpg')
const metaTemplate = this.metaTemplate ?? '%s · OpnForm'

return {
title: title,
titleTemplate: '%s · OpnForm',
titleTemplate: metaTemplate,
meta: [
...(this.metaTags ?? []),
{ vmid: 'og:title', property: 'og:title', content: title },
Expand Down
16 changes: 16 additions & 0 deletions resources/js/pages/forms/show-public.vue
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,28 @@ export default {
return window.location !== window.parent.location || window.frameElement
},
metaTitle () {
if(this.form && this.form.is_pro && this.form.seo_meta.page_title) {
return this.form.seo_meta.page_title
}
return this.form ? this.form.title : 'Create beautiful forms'
},
metaTemplate () {
if (this.form && this.form.is_pro && this.form.seo_meta.page_title) {
// Disable template if custom SEO title
return '%s'
}
return null
},
metaDescription () {
if (this.form && this.form.is_pro && this.form.seo_meta.page_description) {
return this.form.seo_meta.page_description
}
return (this.form && this.form.description) ? this.form.description.substring(0, 160) : null
},
metaImage () {
if (this.form && this.form.is_pro && this.form.seo_meta.page_thumbnail) {
return this.form.seo_meta.page_thumbnail
}
return (this.form && this.form.cover_picture) ? this.form.cover_picture : null
},
metaTags () {
Expand Down

0 comments on commit 01a01a8

Please sign in to comment.