feat: seo tags and keywords for courses and batches

This commit is contained in:
Jannat Patel
2025-05-29 20:13:35 +05:30
parent c3cc0b9bf7
commit d7e550dfea
5 changed files with 207 additions and 38 deletions

View File

@@ -365,7 +365,7 @@ const tabsStructure = computed(() => {
type: 'textarea',
rows: 4,
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',

View File

@@ -273,6 +273,27 @@
/>
</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>
</template>
@@ -302,12 +323,13 @@ import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue'
import { openSettings } from '@/utils'
import { openSettings, getMetaInfo, updateMetaInfo } from '@/utils'
const router = useRouter()
const user = inject('$user')
const { brand } = sessionStore()
const { updateOnboardingStep } = useOnboarding('learning')
const instructors = ref([])
const props = defineProps({
batchName: {
@@ -340,18 +362,26 @@ const batch = reactive({
zoom_account: '',
})
const instructors = ref([])
const meta = reactive({
description: '',
keywords: '',
})
onMounted(() => {
if (!user.data) window.location.href = '/login'
if (props.batchName != 'new') {
batchDetail.reload()
fetchBatchInfo()
} else {
capture('batch_form_opened')
}
window.addEventListener('keydown', keyboardShortcut)
})
const fetchBatchInfo = () => {
batchDetail.reload()
getMetaInfo('batches', props.batchName, meta)
}
const keyboardShortcut = (e) => {
if (
e.key === 's' &&
@@ -465,7 +495,7 @@ const createNewBatch = () => {
localStorage.setItem('firstBatch', data.name)
})
}
updateMetaInfo('batches', data.name, meta)
capture('batch_created')
router.push({
name: 'BatchDetail',
@@ -486,6 +516,7 @@ const editBatchDetails = () => {
{},
{
onSuccess(data) {
updateMetaInfo('batches', data.name, meta)
router.push({
name: 'BatchDetail',
params: {

View File

@@ -76,7 +76,7 @@
<FormControl
v-model="course.short_introduction"
type="textarea"
:rows="4"
:rows="5"
:label="__('Short Introduction')"
:placeholder="
__(
@@ -201,7 +201,7 @@
/>
</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">
{{ __('Pricing and Certification') }}
</div>
@@ -248,6 +248,27 @@
/>
</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 class="border-l">
@@ -264,6 +285,7 @@
<script setup>
import {
Breadcrumbs,
call,
TextEditor,
Button,
createResource,
@@ -287,7 +309,7 @@ import { useRouter } from 'vue-router'
import { capture, startRecording, stopRecording } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import { openSettings } from '@/utils'
import { openSettings, getMetaInfo, updateMetaInfo } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
@@ -328,13 +350,18 @@ const course = reactive({
evaluator: '',
})
const meta = reactive({
description: '',
keywords: '',
})
onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
if (props.courseName !== 'new') {
courseResource.reload()
fetchCourseInfo()
} else {
capture('course_form_opened')
startRecording()
@@ -342,6 +369,11 @@ onMounted(() => {
window.addEventListener('keydown', keyboardShortcut)
})
const fetchCourseInfo = () => {
courseResource.reload()
getMetaInfo('courses', props.courseName, meta)
}
const keyboardShortcut = (e) => {
if (
e.key === 's' &&
@@ -444,40 +476,50 @@ const imageResource = createResource({
const submitCourse = () => {
if (courseResource.data) {
courseEditResource.submit(
{
course: courseResource.data.name,
},
{
onSuccess() {
toast.success(__('Course updated successfully'))
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
}
)
editCourse()
} else {
courseCreationResource.submit(course, {
onSuccess(data) {
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
}
createCourse()
}
}
capture('course_created')
toast.success(__('Course created successfully'))
router.push({
name: 'CourseForm',
params: { courseName: data.name },
const createCourse = () => {
courseCreationResource.submit(course, {
onSuccess(data) {
updateMetaInfo('courses', data.name, meta)
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) {
toast.error(err.messages?.[0] || err)
},
})
}
}
)
}
const deleteCourse = createResource({
@@ -517,7 +559,7 @@ watch(
() => props.courseName !== 'new',
(newVal) => {
if (newVal) {
courseResource.reload()
fetchCourseInfo()
}
}
)

View File

@@ -1,3 +1,5 @@
import { watch } from 'vue'
import { call, toast } from 'frappe-ui'
import { useTimeAgo } from '@vueuse/core'
import { Quiz } from '@/utils/quiz'
import { Assignment } from '@/utils/assignment'
@@ -10,7 +12,6 @@ import Paragraph from '@editorjs/paragraph'
import { CodeBox } from '@/utils/code'
import NestedList from '@editorjs/nested-list'
import InlineCode from '@editorjs/inline-code'
import { watch } from 'vue'
import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed'
import SimpleImage from '@editorjs/simple-image'
@@ -582,3 +583,34 @@ export const cleanError = (message) => {
.replace(/&#x3B;/g, ';')
.replace(/&#x3A;/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)
})
}

View File

@@ -1432,3 +1432,67 @@ def capture_user_persona(responses):
if response.get("message").get("name"):
frappe.db.set_single_value("LMS Settings", "persona_captured", True)
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())