Merge pull request #1547 from pateljannat/seo-in-forms
feat: seo tags and keywords for courses and batches
This commit is contained in:
@@ -365,7 +365,7 @@ const tabsStructure = computed(() => {
|
|||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
rows: 4,
|
rows: 4,
|
||||||
description:
|
description:
|
||||||
'Keywords for search engines to find your website. Separated by commas.',
|
'Comma separated keywords for search engines to find your website.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'Column Break',
|
type: 'Column Break',
|
||||||
|
|||||||
@@ -273,6 +273,27 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="px-20 pb-5 space-y-5 border-b">
|
||||||
|
<div class="text-lg text-ink-gray-9 font-semibold">
|
||||||
|
{{ __('Meta Tags') }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<FormControl
|
||||||
|
v-model="meta.description"
|
||||||
|
:label="__('Meta Description')"
|
||||||
|
type="textarea"
|
||||||
|
:rows="7"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="meta.keywords"
|
||||||
|
:label="__('Meta Keywords')"
|
||||||
|
type="textarea"
|
||||||
|
:rows="7"
|
||||||
|
:placeholder="__('Comma separated keywords for SEO')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -302,12 +323,13 @@ import { useOnboarding } from 'frappe-ui/frappe'
|
|||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import { openSettings } from '@/utils'
|
import { openSettings, getMetaInfo, updateMetaInfo } from '@/utils'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const { updateOnboardingStep } = useOnboarding('learning')
|
const { updateOnboardingStep } = useOnboarding('learning')
|
||||||
|
const instructors = ref([])
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batchName: {
|
batchName: {
|
||||||
@@ -340,18 +362,26 @@ const batch = reactive({
|
|||||||
zoom_account: '',
|
zoom_account: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const instructors = ref([])
|
const meta = reactive({
|
||||||
|
description: '',
|
||||||
|
keywords: '',
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data) window.location.href = '/login'
|
if (!user.data) window.location.href = '/login'
|
||||||
if (props.batchName != 'new') {
|
if (props.batchName != 'new') {
|
||||||
batchDetail.reload()
|
fetchBatchInfo()
|
||||||
} else {
|
} else {
|
||||||
capture('batch_form_opened')
|
capture('batch_form_opened')
|
||||||
}
|
}
|
||||||
window.addEventListener('keydown', keyboardShortcut)
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fetchBatchInfo = () => {
|
||||||
|
batchDetail.reload()
|
||||||
|
getMetaInfo('batches', props.batchName, meta)
|
||||||
|
}
|
||||||
|
|
||||||
const keyboardShortcut = (e) => {
|
const keyboardShortcut = (e) => {
|
||||||
if (
|
if (
|
||||||
e.key === 's' &&
|
e.key === 's' &&
|
||||||
@@ -465,7 +495,7 @@ const createNewBatch = () => {
|
|||||||
localStorage.setItem('firstBatch', data.name)
|
localStorage.setItem('firstBatch', data.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
updateMetaInfo('batches', data.name, meta)
|
||||||
capture('batch_created')
|
capture('batch_created')
|
||||||
router.push({
|
router.push({
|
||||||
name: 'BatchDetail',
|
name: 'BatchDetail',
|
||||||
@@ -486,6 +516,7 @@ const editBatchDetails = () => {
|
|||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
updateMetaInfo('batches', data.name, meta)
|
||||||
router.push({
|
router.push({
|
||||||
name: 'BatchDetail',
|
name: 'BatchDetail',
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
v-model="course.short_introduction"
|
v-model="course.short_introduction"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="5"
|
||||||
:label="__('Short Introduction')"
|
:label="__('Short Introduction')"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
__(
|
__(
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-10 pb-5 space-y-5">
|
<div class="px-10 pb-5 space-y-5 border-b">
|
||||||
<div class="text-lg font-semibold mt-5">
|
<div class="text-lg font-semibold mt-5">
|
||||||
{{ __('Pricing and Certification') }}
|
{{ __('Pricing and Certification') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -248,6 +248,27 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="px-10 pb-5 space-y-5">
|
||||||
|
<div class="text-lg font-semibold mt-5">
|
||||||
|
{{ __('Meta Tags') }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<FormControl
|
||||||
|
v-model="meta.description"
|
||||||
|
:label="__('Meta Description')"
|
||||||
|
type="textarea"
|
||||||
|
:rows="7"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="meta.keywords"
|
||||||
|
:label="__('Meta Keywords')"
|
||||||
|
type="textarea"
|
||||||
|
:rows="7"
|
||||||
|
:placeholder="__('Comma separated keywords for SEO')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l">
|
<div class="border-l">
|
||||||
@@ -264,6 +285,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
|
call,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
Button,
|
Button,
|
||||||
createResource,
|
createResource,
|
||||||
@@ -287,7 +309,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { capture, startRecording, stopRecording } from '@/telemetry'
|
import { capture, startRecording, stopRecording } from '@/telemetry'
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import { openSettings } from '@/utils'
|
import { openSettings, getMetaInfo, updateMetaInfo } from '@/utils'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
@@ -328,13 +350,18 @@ const course = reactive({
|
|||||||
evaluator: '',
|
evaluator: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const meta = reactive({
|
||||||
|
description: '',
|
||||||
|
keywords: '',
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||||
router.push({ name: 'Courses' })
|
router.push({ name: 'Courses' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.courseName !== 'new') {
|
if (props.courseName !== 'new') {
|
||||||
courseResource.reload()
|
fetchCourseInfo()
|
||||||
} else {
|
} else {
|
||||||
capture('course_form_opened')
|
capture('course_form_opened')
|
||||||
startRecording()
|
startRecording()
|
||||||
@@ -342,6 +369,11 @@ onMounted(() => {
|
|||||||
window.addEventListener('keydown', keyboardShortcut)
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fetchCourseInfo = () => {
|
||||||
|
courseResource.reload()
|
||||||
|
getMetaInfo('courses', props.courseName, meta)
|
||||||
|
}
|
||||||
|
|
||||||
const keyboardShortcut = (e) => {
|
const keyboardShortcut = (e) => {
|
||||||
if (
|
if (
|
||||||
e.key === 's' &&
|
e.key === 's' &&
|
||||||
@@ -444,40 +476,50 @@ const imageResource = createResource({
|
|||||||
|
|
||||||
const submitCourse = () => {
|
const submitCourse = () => {
|
||||||
if (courseResource.data) {
|
if (courseResource.data) {
|
||||||
courseEditResource.submit(
|
editCourse()
|
||||||
{
|
|
||||||
course: courseResource.data.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
toast.success(__('Course updated successfully'))
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
toast.error(err.messages?.[0] || err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
courseCreationResource.submit(course, {
|
createCourse()
|
||||||
onSuccess(data) {
|
}
|
||||||
if (user.data?.is_system_manager) {
|
}
|
||||||
updateOnboardingStep('create_first_course', true, false, () => {
|
|
||||||
localStorage.setItem('firstCourse', data.name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
capture('course_created')
|
const createCourse = () => {
|
||||||
toast.success(__('Course created successfully'))
|
courseCreationResource.submit(course, {
|
||||||
router.push({
|
onSuccess(data) {
|
||||||
name: 'CourseForm',
|
updateMetaInfo('courses', data.name, meta)
|
||||||
params: { courseName: data.name },
|
if (user.data?.is_system_manager) {
|
||||||
|
updateOnboardingStep('create_first_course', true, false, () => {
|
||||||
|
localStorage.setItem('firstCourse', data.name)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
capture('course_created')
|
||||||
|
toast.success(__('Course created successfully'))
|
||||||
|
router.push({
|
||||||
|
name: 'CourseForm',
|
||||||
|
params: { courseName: data.name },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editCourse = () => {
|
||||||
|
courseEditResource.submit(
|
||||||
|
{
|
||||||
|
course: courseResource.data.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
updateMetaInfo('courses', props.courseName, meta)
|
||||||
|
toast.success(__('Course updated successfully'))
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
toast.error(err.messages?.[0] || err)
|
toast.error(err.messages?.[0] || err)
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCourse = createResource({
|
const deleteCourse = createResource({
|
||||||
@@ -517,7 +559,7 @@ watch(
|
|||||||
() => props.courseName !== 'new',
|
() => props.courseName !== 'new',
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
courseResource.reload()
|
fetchCourseInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { watch } from 'vue'
|
||||||
|
import { call, toast } from 'frappe-ui'
|
||||||
import { useTimeAgo } from '@vueuse/core'
|
import { useTimeAgo } from '@vueuse/core'
|
||||||
import { Quiz } from '@/utils/quiz'
|
import { Quiz } from '@/utils/quiz'
|
||||||
import { Assignment } from '@/utils/assignment'
|
import { Assignment } from '@/utils/assignment'
|
||||||
@@ -10,7 +12,6 @@ import Paragraph from '@editorjs/paragraph'
|
|||||||
import { CodeBox } from '@/utils/code'
|
import { CodeBox } from '@/utils/code'
|
||||||
import NestedList from '@editorjs/nested-list'
|
import NestedList from '@editorjs/nested-list'
|
||||||
import InlineCode from '@editorjs/inline-code'
|
import InlineCode from '@editorjs/inline-code'
|
||||||
import { watch } from 'vue'
|
|
||||||
import dayjs from '@/utils/dayjs'
|
import dayjs from '@/utils/dayjs'
|
||||||
import Embed from '@editorjs/embed'
|
import Embed from '@editorjs/embed'
|
||||||
import SimpleImage from '@editorjs/simple-image'
|
import SimpleImage from '@editorjs/simple-image'
|
||||||
@@ -582,3 +583,34 @@ export const cleanError = (message) => {
|
|||||||
.replace(/;/g, ';')
|
.replace(/;/g, ';')
|
||||||
.replace(/:/g, ':')
|
.replace(/:/g, ':')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getMetaInfo = (type, route, meta) => {
|
||||||
|
call('lms.lms.api.get_meta_info', {
|
||||||
|
type: type,
|
||||||
|
route: route,
|
||||||
|
}).then((data) => {
|
||||||
|
if (data.length) {
|
||||||
|
data.forEach((row) => {
|
||||||
|
if (row.key == 'description') {
|
||||||
|
meta.description = row.value
|
||||||
|
} else if (row.key == 'keywords') {
|
||||||
|
meta.keywords = row.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMetaInfo = (type, route, meta) => {
|
||||||
|
call('lms.lms.api.update_meta_info', {
|
||||||
|
type: type,
|
||||||
|
route: route,
|
||||||
|
meta_tags: [
|
||||||
|
{ key: 'description', value: meta.description },
|
||||||
|
{ key: 'keywords', value: meta.keywords },
|
||||||
|
],
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(__('Failed to update meta tags {0}').format(error))
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1432,3 +1432,67 @@ def capture_user_persona(responses):
|
|||||||
if response.get("message").get("name"):
|
if response.get("message").get("name"):
|
||||||
frappe.db.set_single_value("LMS Settings", "persona_captured", True)
|
frappe.db.set_single_value("LMS Settings", "persona_captured", True)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_meta_info(type, route):
|
||||||
|
if frappe.db.exists("Website Meta Tag", {"parent": f"{type}/{route}"}):
|
||||||
|
meta_tags = frappe.get_all(
|
||||||
|
"Website Meta Tag",
|
||||||
|
{
|
||||||
|
"parent": f"{type}/{route}",
|
||||||
|
},
|
||||||
|
["name", "key", "value"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return meta_tags
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_meta_info(type, route, meta_tags):
|
||||||
|
parent_name = f"{type}/{route}"
|
||||||
|
if not isinstance(meta_tags, list):
|
||||||
|
frappe.throw(_("Meta tags should be a list."))
|
||||||
|
|
||||||
|
for tag in meta_tags:
|
||||||
|
existing_tag = frappe.db.exists(
|
||||||
|
"Website Meta Tag",
|
||||||
|
{
|
||||||
|
"parent": parent_name,
|
||||||
|
"parenttype": "Website Route Meta",
|
||||||
|
"parentfield": "meta_tags",
|
||||||
|
"key": tag["key"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if existing_tag:
|
||||||
|
if not tag.get("value"):
|
||||||
|
frappe.db.delete("Website Meta Tag", existing_tag)
|
||||||
|
continue
|
||||||
|
frappe.db.set_value("Website Meta Tag", existing_tag, "value", tag["value"])
|
||||||
|
elif tag.get("value"):
|
||||||
|
tag_properties = {
|
||||||
|
"parent": parent_name,
|
||||||
|
"parenttype": "Website Route Meta",
|
||||||
|
"parentfield": "meta_tags",
|
||||||
|
"key": tag["key"],
|
||||||
|
"value": tag["value"],
|
||||||
|
}
|
||||||
|
|
||||||
|
parent_exists = frappe.db.exists("Website Route Meta", parent_name)
|
||||||
|
if not parent_exists:
|
||||||
|
route_meta = frappe.new_doc("Website Route Meta")
|
||||||
|
route_meta.update(
|
||||||
|
{
|
||||||
|
"__newname": parent_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
route_meta.append("meta_tags", tag_properties)
|
||||||
|
route_meta.insert()
|
||||||
|
else:
|
||||||
|
new_tag = frappe.new_doc("Website Meta Tag")
|
||||||
|
new_tag.update(tag_properties)
|
||||||
|
print(new_tag)
|
||||||
|
new_tag.insert()
|
||||||
|
print(new_tag.as_dict())
|
||||||
|
|||||||
Reference in New Issue
Block a user