Compare commits

...

2 Commits

@ -60,6 +60,7 @@ class PostController extends XController
$post->is_pinned = $request->has('is_pin'); $post->is_pinned = $request->has('is_pin');
$post->table_of_contents = $request->has('table_of_contents'); $post->table_of_contents = $request->has('table_of_contents');
$post->icon = $request->input('icon'); $post->icon = $request->input('icon');
$post->keyword = $request->input('keyword');
if ($request->has('canonical') && trim($request->input('canonical')) != ''){ if ($request->has('canonical') && trim($request->input('canonical')) != ''){
$post->canonical = $request->input('canonical'); $post->canonical = $request->input('canonical');

@ -57,6 +57,7 @@ class ProductController extends XController
$product->table = $request->input('table'); $product->table = $request->input('table');
$product->description = $request->input('desc'); $product->description = $request->input('desc');
$product->excerpt = $request->input('excerpt'); $product->excerpt = $request->input('excerpt');
$product->keyword = $request->input('keyword');
$product->stock_status = $request->input('stock_status'); $product->stock_status = $request->input('stock_status');
$product->price = $request->input('price',0); $product->price = $request->input('price',0);
$product->buy_price = $request->input('buy_price',0); $product->buy_price = $request->input('buy_price',0);

@ -30,6 +30,7 @@ return new class extends Migration
$table->json('theme')->nullable(); $table->json('theme')->nullable();
$table->text('canonical')->nullable(); $table->text('canonical')->nullable();
$table->string('promote')->nullable(); $table->string('promote')->nullable();
$table->text('keyword')->nullable();
$table->softDeletes(); $table->softDeletes();
$table->timestamps(); $table->timestamps();

@ -38,6 +38,7 @@ return new class extends Migration
$table->json('theme')->nullable(); $table->json('theme')->nullable();
$table->text('canonical')->nullable(); $table->text('canonical')->nullable();
$table->string('promote')->nullable(); $table->string('promote')->nullable();
$table->text('keyword')->nullable();
$table->softDeletes(); $table->softDeletes();
$table->timestamps(); $table->timestamps();

@ -31,6 +31,7 @@ import './panel/sotable-controller.js';
import './panel/prototypes.js'; import './panel/prototypes.js';
import './panel/panel-window-loader.js'; import './panel/panel-window-loader.js';
import './panel/responsive-control.js'; import './panel/responsive-control.js';
// import './panel/seo-analyzer.js';
// chartjs.defaults.defaultFontFamily = "Vazir"; // chartjs.defaults.defaultFontFamily = "Vazir";
// chartjs.defaults.defaultFontSize = 18; // chartjs.defaults.defaultFontSize = 18;

@ -1,4 +1,8 @@
import ContentSEOAnalyzer from './seo-analyzer.js'
let timeOut = null;
window.addEventListener('load', function () { window.addEventListener('load', function () {
let keywordInput = document.querySelector('#keyword') ;
let dirx = document.querySelector('#panel-dir').value; let dirx = document.querySelector('#panel-dir').value;
let editors = {}; let editors = {};
document.querySelectorAll('.ckeditorx')?.forEach(function (el) { document.querySelectorAll('.ckeditorx')?.forEach(function (el) {
@ -15,9 +19,27 @@ window.addEventListener('load', function () {
skin: 'moono-dark', skin: 'moono-dark',
}); });
CKEDITOR.addCss('.cke_editable { background-color: ' + website_bg + '; color: ' + website_text_color + ' ; font-family: ' + website_font + ' }'); CKEDITOR.addCss('.cke_editable { background-color: ' + website_bg + '; color: ' + website_text_color + ' ; font-family: ' + website_font + ' }');
editors[el.getAttribute('name')].on('change', function (evt) { editors[el.getAttribute('name')].on('change', function (evt) {
el.value = evt.editor.getData(); const content = evt.editor.getData();
el.value = content;
if (el.classList.contains('seo-analyze')){
let keyword = keywordInput?.value;
const analyzer = new ContentSEOAnalyzer(content, keyword);
const report = analyzer.generateReport();
analyzer.displaySEOReport(report,'seo-hint')
}
}); });
if (el.classList.contains('seo-analyze')){
editors[el.getAttribute('name')].fire('change');
keywordInput?.addEventListener('input',function () {
editors[el.getAttribute('name')].fire('change');
});
}
}); });
}); });

@ -0,0 +1,370 @@
class ContentSEOAnalyzer {
constructor(content, targetKeyword) {
this.content = content;
this.targetKeyword = targetKeyword.toLowerCase();
this.plainText = this.stripHTML(content);
this.sentences = this.getSentences();
this.paragraphs = this.getParagraphs();
this.wordCount = this.getWordCount();
}
// Remove HTML tags and get plain text
stripHTML(html) {
return html.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
// Improved sentence detection for mixed content
getSentences() {
// First clean the text from extra spaces and normalize punctuation
let text = this.plainText
.replace(/\s+/g, ' ')
.replace(/[\u200B-\u200D\uFEFF]/g, ''); // Remove zero-width spaces
// Handle both RTL and LTR sentence endings
// Added more Arabic/Persian punctuation marks
const sentenceEndings = [
'.', // English period
'!', // English exclamation
'?', // English question mark
'؟', // Arabic question mark
'।', // Arabic full stop
'۔', // Urdu full stop
'،', // Arabic comma when followed by a new sentence
';', // English semicolon when used as sentence separator
'؛', // Arabic semicolon
];
// Create a regex pattern that matches any of these endings
// followed by a space and either:
// 1. An uppercase letter (for English)
// 2. An Arabic/Persian letter
// 3. A number (for both scripts)
const pattern = new RegExp(
`([${sentenceEndings.join('')}])\\s*(?=[A-Z\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF0-9])`,
'g'
);
// Split into sentences
let sentences = text
.replace(pattern, '$1|')
.split('|')
.map(sentence => sentence.trim())
.filter(sentence => {
// Remove empty sentences and very short ones (less than 2 words)
const words = sentence.split(/\s+/);
return sentence.length > 0 && words.length >= 2;
});
// Additional cleaning: merge incorrectly split sentences
sentences = this.cleanSentences(sentences);
return sentences;
}
// Helper method to clean and merge sentences that might have been incorrectly split
cleanSentences(sentences) {
const cleaned = [];
let current = '';
for (let sentence of sentences) {
// Check if sentence starts with lowercase or is very short
if (current && (
sentence.charAt(0).match(/[a-z]/) || // Starts with lowercase
sentence.length < 10 || // Very short
/^[و،]/.test(sentence) // Starts with Arabic 'and' or comma
)) {
current += ' ' + sentence;
} else {
if (current) {
cleaned.push(current.trim());
}
current = sentence;
}
}
// Don't forget to add the last sentence
if (current) {
cleaned.push(current.trim());
}
// Final filtering to remove any remaining invalid sentences
return cleaned.filter(sentence => {
// Ensure minimum length and word count
const words = sentence.split(/\s+/);
return sentence.length >= 10 && words.length >= 2;
});
}
// Adjust the readability analysis to be more accurate with the new sentence detection
analyzeReadability() {
const avgSentenceLength = this.sentences.length ?
this.wordCount / this.sentences.length : 0;
const avgParagraphLength = this.paragraphs.length ?
this.wordCount / this.paragraphs.length : 0;
// Increased threshold for complex sentences
const complexSentences = this.sentences.filter(sentence =>
sentence.split(/\s+/).filter(word => word.length > 0).length > 25
).length;
const complexSentencePercentage = this.sentences.length ?
(complexSentences / this.sentences.length) * 100 : 0;
return {
avgSentenceLength,
avgParagraphLength,
complexSentencePercentage,
totalParagraphs: this.paragraphs.length,
totalSentences: this.sentences.length
};
}
// Helper method to calculate statistical variation in sentence lengths
calculateVariation(lengths) {
if (lengths.length < 2) return 0;
const mean = lengths.reduce((sum, val) => sum + val, 0) / lengths.length;
const variance = lengths.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / lengths.length;
return Math.sqrt(variance);
}
// Calculate a more nuanced readability score
calculateReadabilityScore(sentenceLengths) {
if (sentenceLengths.length === 0) return 0;
const avg = sentenceLengths.reduce((sum, len) => sum + len, 0) / sentenceLengths.length;
const variation = this.calculateVariation(sentenceLengths);
// Ideal ranges:
// Average sentence length: 15-20 words
// Variation: 5-10 words (some variety but not too much)
let score = 10;
// Penalize for extreme average lengths
if (avg < 10) score -= 2;
else if (avg > 25) score -= 2;
else if (avg > 20) score -= 1;
// Penalize for too much or too little variation
if (variation < 3) score -= 1; // Too monotonous
else if (variation > 15) score -= 1; // Too varied
return Math.max(0, Math.min(10, score));
}
// Get paragraphs from content
getParagraphs() {
return this.content
.split(/<\/p>|<\/div>|<br\s*\/?>|\n/)
.map(p => this.stripHTML(p))
.filter(p => p.trim().length > 0);
}
// Get word count
getWordCount() {
return this.plainText.split(/\s+/).filter(word => word.length > 0).length;
}
// Calculate average sentence length
getAverageSentenceLength() {
if (this.sentences.length === 0) return 0;
const totalWords = this.sentences
.map(sentence => sentence.split(/\s+/).filter(word => word.length > 0).length)
.reduce((sum, length) => sum + length, 0);
return totalWords / this.sentences.length;
}
// Calculate average paragraph length
getAverageParagraphLength() {
if (this.paragraphs.length === 0) return 0;
const totalWords = this.paragraphs
.map(para => para.split(/\s+/).filter(word => word.length > 0).length)
.reduce((sum, length) => sum + length, 0);
return totalWords / this.paragraphs.length;
}
// Analyze keyword usage
analyzeKeyword() {
// Check keyword size
let shortKeyword = false;
if (this.targetKeyword.length < 2){
shortKeyword = true;
}
const keywordCount = (this.plainText.toLowerCase().match(new RegExp(this.targetKeyword, 'g')) || []).length;
const density = (keywordCount / this.wordCount) * 100;
// Check keyword in first paragraph
const firstParagraphHasKeyword = this.paragraphs[0]?.toLowerCase().includes(this.targetKeyword);
// Check keyword in headings
const headings = this.content.match(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi) || [];
const headingsWithKeyword = headings.filter(h =>
this.stripHTML(h).toLowerCase().includes(this.targetKeyword)
).length;
return {
count: keywordCount,
density,
firstParagraphHasKeyword,
headingsWithKeyword,
shortKeyword: shortKeyword,
};
}
// Generate analysis report with 0-10 rating
generateReport() {
const keywordAnalysis = this.analyzeKeyword();
const readabilityAnalysis = this.analyzeReadability();
let score = 0;
const feedback = [];
// Score components (each component adds up to 10)
// 1. Content Length (2 points)
if (this.wordCount >= 300) score += 2;
else feedback.push(window.TR.contentShort);
// 2. Keyword Usage (2 points)
if (keywordAnalysis.density >= 0.5 && keywordAnalysis.density <= 2.5) score += 0.5;
if (keywordAnalysis.firstParagraphHasKeyword) score += 0.5;
if (keywordAnalysis.headingsWithKeyword > 0) score += 1;
if (keywordAnalysis.count >= 2) score += 0;
if (keywordAnalysis.density < 0.5) feedback.push(window.TR.destinyLow);
if (keywordAnalysis.density > 3.5) feedback.push(window.TR.destinyHigh);
if (keywordAnalysis.shortKeyword) feedback.push(window.TR.shortKeyword);
if (!keywordAnalysis.firstParagraphHasKeyword) feedback.push(window.TR.keywordFirstParagraph);
if (keywordAnalysis.headingsWithKeyword === 0) feedback.push(window.TR.keywordHeading);
// 3. Readability (4 points)
if (readabilityAnalysis.avgSentenceLength <= 30) score += 1;
if (readabilityAnalysis.avgParagraphLength <= 150) score += 1;
if (readabilityAnalysis.complexSentencePercentage <= 25) score += 1;
if (this.paragraphs.length >= 3) score += 1;
if (readabilityAnalysis.avgSentenceLength > 30) feedback.push(window.TR.sentencesLong);
if (readabilityAnalysis.avgParagraphLength > 150) feedback.push(window.TR.paragraphsLong);
if (readabilityAnalysis.complexSentencePercentage > 25) feedback.push(window.TR.sentencesComplex);
if (this.paragraphs.length < 3) feedback.push(window.TR.paragraphAdd);
// 4. Structure & Formatting (2 points)
const hasHeadings = /<h[1-6][^>]*>/i.test(this.content);
const hasLists = /<[ou]l[^>]*>/i.test(this.content);
if (hasHeadings) score += 1;
if (hasLists) score += 1;
if (!hasHeadings) feedback.push(window.TR.headingAdd );
if (!hasLists) feedback.push(window.TR.useList);
console.log(readabilityAnalysis);
return {
score: Math.min(10, Math.round(score * 10) / 10),
feedback,
details: {
wordCount: this.wordCount,
keywordUsage: {
count: keywordAnalysis.count,
density: `${keywordAnalysis.density.toFixed(1)}%`,
inFirstParagraph: keywordAnalysis.firstParagraphHasKeyword,
inHeadings: keywordAnalysis.headingsWithKeyword
},
readability: {
avgWordsPerSentence: Math.round(readabilityAnalysis.avgSentenceLength),
avgWordsPerParagraph: Math.round(readabilityAnalysis.avgParagraphLength),
complexSentences: `${readabilityAnalysis.complexSentencePercentage.toFixed(1)}%`,
paragraphCount: readabilityAnalysis.totalParagraphs
}
}
};
}
// Function to determine score status
getScoreStatus(score) {
if (score >= 7) return { class: 'good', text: window.TR.good };
if (score >= 5) return { class: 'average', text: window.TR.averageNeeed };
return { class: 'poor', text: window.TR.poor };
}
// Function to create and display the report
displaySEOReport(report, targetElement) {
// // Add styles to document if not already present
// if (!document.getElementById('seo-report-styles')) {
// const styleSheet = document.createElement('style');
// styleSheet.id = 'seo-report-styles';
// styleSheet.textContent = styles;
// document.head.appendChild(styleSheet);
// }
const scoreStatus = this.getScoreStatus(report.score);
const reportHTML = `
<div class="seo-report">
<div class="seo-score-container">
<div class="seo-score">
<div class="seo-score-circle ${scoreStatus.class}">
${report.score.toFixed(1)}
</div>
</div>
<div class="seo-status">
<h3>${ window.TR.SEOScore}: ${scoreStatus.text}</h3>
<p>${window.TR.basedOnKeyword}</p>
</div>
</div>
<div class="seo-details">
<div class="seo-feedback">
<h4>${window.TR.recommendations}</h4>
<ul>
${report.feedback.map(item => `<li>${item}</li>`).join('')}
</ul>
</div>
<div class="seo-metrics">
<div class="metric-card">
<h4>${window.TR.contentLength}</h4>
<div class="metric-value">${report.details.wordCount} ${ window.TR.words}</div>
</div>
<div class="metric-card">
<h4>${window.TR.keywordUsage}</h4>
<div class="metric-value">
${report.details.keywordUsage.count} ${ window.TR.times}
(${report.details.keywordUsage.density})
</div>
</div>
<div class="metric-card">
<h4>${window.TR.avgSenLen}</h4>
<div class="metric-value">
${report.details.readability.avgWordsPerSentence} ${ window.TR.words}
</div>
</div>
<div class="metric-card">
<h4>${window.TR.avgParaStruc}</h4>
<div class="metric-value">
${report.details.readability.paragraphCount} ${ window.TR.paragraphs}
(avg ${report.details.readability.avgWordsPerParagraph} ${ window.TR.words})
</div>
</div>
</div>
</div>
</div>
`;
const targetDiv = document.getElementById(targetElement);
if (targetDiv) {
targetDiv.innerHTML = reportHTML;
}
}
}
export default ContentSEOAnalyzer;

@ -25,6 +25,8 @@
"Add": "افزودن", "Add": "افزودن",
"Add another one": "افزودن یک مورد دیگر", "Add another one": "افزودن یک مورد دیگر",
"Add cover to better results": "یک کاور جهت بهتر شدن نتایج اضافه کنید", "Add cover to better results": "یک کاور جهت بهتر شدن نتایج اضافه کنید",
"Add headings to structure your content": "سر تیتر یا heading به محتوای خود اضافه کنید",
"Add more paragraphs to improve structure": "برای بهینه تر شدم بندهای بیشتری به نوشته خود اضافه کنید",
"Add new adv": "افزودن یک تبلیغ", "Add new adv": "افزودن یک تبلیغ",
"Add new attachment": "افزودن یک پیوست", "Add new attachment": "افزودن یک پیوست",
"Add new category": "افزودن یک دسته", "Add new category": "افزودن یک دسته",
@ -93,9 +95,11 @@
"Auth code send successfully": "کد احراز هویت ارسال شد", "Auth code send successfully": "کد احراز هویت ارسال شد",
"Authentication Mail": "ایمیل احراز هویت", "Authentication Mail": "ایمیل احراز هویت",
"Avatar": "آواتار", "Avatar": "آواتار",
"Average Sentence Length": "میانگین طول جملات",
"Back to profile": "بازگشت به نمایه", "Back to profile": "بازگشت به نمایه",
"Background image": "تصویر زمینه", "Background image": "تصویر زمینه",
"Base price": "مبلغ پایه", "Base price": "مبلغ پایه",
"Based on content analysis and keyword optimization": "بر اساس تحلیل محتوا و بهینه سازی کلیدواژگان",
"Basic data": "اطلاعات پایه", "Basic data": "اطلاعات پایه",
"Batch delete": "حذف چندگانه", "Batch delete": "حذف چندگانه",
"Batch restore": "بازیافت چندگانه", "Batch restore": "بازیافت چندگانه",
@ -137,8 +141,11 @@
"Compare": "مقایسه", "Compare": "مقایسه",
"Compare products": "مقایسه محصولات", "Compare products": "مقایسه محصولات",
"Confirm Password": "تایید گذرواژه", "Confirm Password": "تایید گذرواژه",
"Consider using lists to improve readability": "استفاده از لیست ها را برای بهبود خوانایی در نظر بگیرید",
"Contact us": "تماس با ما", "Contact us": "تماس با ما",
"Contacts list": "فهرست تماس‌ها", "Contacts list": "فهرست تماس‌ها",
"Content Length": "طول محتوا",
"Content is too short. Aim for at least 300 words": "محتوا بسیار کوتاه است، برای نتیجه بهتر حداقل به ۳۰۰ واژه نیاز دارید",
"Contents": "محتوا", "Contents": "محتوا",
"Continue": "ادامه", "Continue": "ادامه",
"Count": "تعداد", "Count": "تعداد",
@ -240,6 +247,7 @@
"Gallery": "گالری", "Gallery": "گالری",
"General": "عمومی", "General": "عمومی",
"Gfxes": "طراحی‌ها", "Gfxes": "طراحی‌ها",
"Good": "خوب",
"Graphic": "گرافیک", "Graphic": "گرافیک",
"Group": "سرفصل", "Group": "سرفصل",
"Group Parent": "والد سرفصل", "Group Parent": "والد سرفصل",
@ -265,6 +273,8 @@
"Image deleted successfully": "تصویر حذف شد", "Image deleted successfully": "تصویر حذف شد",
"Image uploaded successfully": "تصویر افزوده شد", "Image uploaded successfully": "تصویر افزوده شد",
"Images": "تصاویر", "Images": "تصاویر",
"Include keyword in at least one heading": "حداقل یک مرتبه کلیدواژه را به عنوان اضافه کنید",
"Include keyword in the first paragraph": "کلیدواژه را در بند (پاراگراف) اول اضافه نمایید",
"Increase \/ decrease by Admin": "افزودن \/ کاهش توسط مدیر سایت", "Increase \/ decrease by Admin": "افزودن \/ کاهش توسط مدیر سایت",
"Increase by Admin removed:": "افزودنت توسط مدیر حذف شد", "Increase by Admin removed:": "افزودنت توسط مدیر حذف شد",
"Index": "صفحه نخست", "Index": "صفحه نخست",
@ -284,6 +294,11 @@
"Is fillable": "قابل نمایش", "Is fillable": "قابل نمایش",
"Item": "آیتم", "Item": "آیتم",
"Key": "کلید", "Key": "کلید",
"Keyword": "کلیدواژه کانونی",
"Keyword Usage": "استفاده از کلیدواژه",
"Keyword density is too high": "تراکم کلید واژه بسیار است",
"Keyword density is too low": "تراکم کلیدواژه کم است",
"Keyword is too short fix keyword to better analyze": "کلیدواژه بسیار کوتاه است، برای بررسی بهتر اصلاح نمایید",
"Label": "برچسب", "Label": "برچسب",
"Languages": "زبان‌ها", "Languages": "زبان‌ها",
"Languages list": "فهرست زبان‌ها", "Languages list": "فهرست زبان‌ها",
@ -321,12 +336,15 @@
"Name": "نام", "Name": "نام",
"Name and lastname": "نام و نام‌خانوادگی", "Name and lastname": "نام و نام‌خانوادگی",
"Need process orders": "سفارشات نیازمند رسیدگی", "Need process orders": "سفارشات نیازمند رسیدگی",
"Needs Improvement": "نیازمند بهینه‌تر شدن",
"Next": "بعدی", "Next": "بعدی",
"No parent": "بدون والد", "No parent": "بدون والد",
"Not required": "غیر ضرروری", "Not required": "غیر ضرروری",
"Order removed successfully": "سفارش با موفقیت حذف شد", "Order removed successfully": "سفارش با موفقیت حذف شد",
"Orders": "سفارشاات", "Orders": "سفارشاات",
"Orders count": "تعداد سفارش", "Orders count": "تعداد سفارش",
"Paragraph Structure": "ساختار بند",
"Paragraphs are too long": "بندها بسیار طولانی هستند",
"Password": "گذرواژه", "Password": "گذرواژه",
"Password or new password is:": "گذرواژه یا گذرواژه جدید عبارت است از:", "Password or new password is:": "گذرواژه یا گذرواژه جدید عبارت است از:",
"Pay now": "پرداخت", "Pay now": "پرداخت",
@ -339,6 +357,7 @@
"Please confirm your password before continuing.": "لطفا پیش از ادامه گذرواژه خود را تایید کنید", "Please confirm your password before continuing.": "لطفا پیش از ادامه گذرواژه خود را تایید کنید",
"Please upload file": "لطفا یک پرونده بارگزاری کنید", "Please upload file": "لطفا یک پرونده بارگزاری کنید",
"Please, Login or complete information to pay": "لطفا وارد شوید یا اطلاعات خود را تکمیل کنید تا اجازه پرداخت داشته باشید", "Please, Login or complete information to pay": "لطفا وارد شوید یا اطلاعات خود را تکمیل کنید تا اجازه پرداخت داشته باشید",
"Poor": "ضعیف",
"Post": "نوشته", "Post": "نوشته",
"Post Text": "متن نوشته", "Post Text": "متن نوشته",
"Post code": "کد پستی", "Post code": "کد پستی",
@ -353,8 +372,8 @@
"Print": "چاپ", "Print": "چاپ",
"Product": "محصول", "Product": "محصول",
"Product added to compare": "محصول به فهرست مقایسه افزوده شد", "Product added to compare": "محصول به فهرست مقایسه افزوده شد",
"Product grid": "کاشی محصول",
"Product added to favorites": "محصول به علاقه‌مندی شما افزوده شد", "Product added to favorites": "محصول به علاقه‌مندی شما افزوده شد",
"Product grid": "کاشی محصول",
"Product removed from compare": "محصول از فهرست مقایسه حذف شد", "Product removed from compare": "محصول از فهرست مقایسه حذف شد",
"Product removed from favorites": "محصول از علاقه مندی های شما حذف شد", "Product removed from favorites": "محصول از علاقه مندی های شما حذف شد",
"Product table": "جدول محصول", "Product table": "جدول محصول",
@ -376,10 +395,11 @@
"Questions list": "فهرست سوالات", "Questions list": "فهرست سوالات",
"RTL": "راست به چپ", "RTL": "راست به چپ",
"Rate": "امتیاز", "Rate": "امتیاز",
"Rates list": "فهرست امتیازها",
"Rates": "امتیازها", "Rates": "امتیازها",
"Rates list": "فهرست امتیازها",
"Read more": "اطلاعات بیشتر", "Read more": "اطلاعات بیشتر",
"Recent posts": "واپسین نوشته‌ها", "Recent posts": "واپسین نوشته‌ها",
"Recommendations": "توصیه‌ها",
"Recommends": "توصیه‌ها", "Recommends": "توصیه‌ها",
"Register": "ثبت‌نام", "Register": "ثبت‌نام",
"Register or Reset password": "ثبت‌نام یا بازیابی گذرواژه", "Register or Reset password": "ثبت‌نام یا بازیابی گذرواژه",
@ -399,6 +419,7 @@
"Role": "نقش", "Role": "نقش",
"Role filter": "صافی نقش‌ها", "Role filter": "صافی نقش‌ها",
"SEO": "سئو", "SEO": "سئو",
"SEO Score": "امتیاز سئو",
"SKU": "", "SKU": "",
"SMS": "پیامک", "SMS": "پیامک",
"Save": "ذخیره", "Save": "ذخیره",
@ -406,7 +427,7 @@
"Search": "جستجو", "Search": "جستجو",
"Search & Filter": "جستجو و صافی", "Search & Filter": "جستجو و صافی",
"Search for": "جستجو برای", "Search for": "جستجو برای",
"Search word is too short": "کلمه مورد جستجو بسیار کوتاه است", "Search word is too short": "واژه مورد جستجو بسیار کوتاه است",
"Searchable": "قابل جستجو", "Searchable": "قابل جستجو",
"Section": "بخش", "Section": "بخش",
"Sections": "بخش‌ها", "Sections": "بخش‌ها",
@ -418,6 +439,7 @@
"Send authenticate code": "ارسال کد احراز هویت", "Send authenticate code": "ارسال کد احراز هویت",
"Send ticket": "ارسال تیکت", "Send ticket": "ارسال تیکت",
"Sent to": "ارسال به", "Sent to": "ارسال به",
"Sentences are too long": "جملات بسیار طولانی هستند",
"Set": "تغییر به", "Set": "تغییر به",
"Setting": "تنظیمات", "Setting": "تنظیمات",
"Setting added to website": "تنظیم به سایت اضافه شد", "Setting added to website": "تنظیم به سایت اضافه شد",
@ -436,8 +458,8 @@
"Signed out successfully": "با موفقیت خارج شدید", "Signed out successfully": "با موفقیت خارج شدید",
"Size": "اندازه", "Size": "اندازه",
"Slider": "اسلایدر", "Slider": "اسلایدر",
"Sliders": "اسلایدرها",
"Slider data": "اطلاعات اسلایدر", "Slider data": "اطلاعات اسلایدر",
"Sliders": "اسلایدرها",
"Sliders list": "فهرست اسلایدر", "Sliders list": "فهرست اسلایدر",
"Slug": "نامک", "Slug": "نامک",
"Sort": "مرتب", "Sort": "مرتب",
@ -478,6 +500,7 @@
"Toggle favorite": "تغییر وضعیت علاقه‌مندی", "Toggle favorite": "تغییر وضعیت علاقه‌مندی",
"Toggle navigation": "", "Toggle navigation": "",
"Toggle selection": "برعکس کردن انتخاب", "Toggle selection": "برعکس کردن انتخاب",
"Too many complex sentences": "جملات پیچیده بسیار هستند",
"Total": "همه", "Total": "همه",
"Total price": "مبلغ کل", "Total price": "مبلغ کل",
"Tracking code": "کد پیگیری", "Tracking code": "کد پیگیری",
@ -562,21 +585,21 @@
"canonical": "کنونیکال", "canonical": "کنونیکال",
"category": "دسته‌بندی", "category": "دسته‌بندی",
"category_id": "دسته‌بندی", "category_id": "دسته‌بندی",
"click here to request another": "برای ایجاد درخواست دیگر اینجا کلیک کنید",
"code": "کد", "code": "کد",
"count": "تعداد", "count": "تعداد",
"country": "کشور", "country": "کشور",
"click here to request another": "برای ایجاد درخواست دیگر اینجا کلیک کنید",
"created_at": "زمان ایجاد", "created_at": "زمان ایجاد",
"customer_id": "مشتری", "customer_id": "مشتری",
"email": "رایانامه", "email": "رایانامه",
"emoji": "ایموجی", "emoji": "ایموجی",
"error in payment.": "خطا در پرداخت", "error in payment.": "خطا در پرداخت",
"error in payment. contact admin.": "خطا در پرداخت با مدیر وبسایت تماس بگیرید", "error in payment. contact admin.": "خطا در پرداخت با مدیر وبسایت تماس بگیرید",
"expire": "انقضا",
"evaluation": "ارزیابی", "evaluation": "ارزیابی",
"evaluation_id": "ارزیابی", "evaluation_id": "ارزیابی",
"group_id": "سرفصل", "expire": "انقضا",
"group": "سرفصل", "group": "سرفصل",
"group_id": "سرفصل",
"hash": "هش", "hash": "هش",
"icon": "نماد", "icon": "نماد",
"image": "تصویر", "image": "تصویر",
@ -585,8 +608,8 @@
"is_default": "پیش فرض", "is_default": "پیش فرض",
"jpg": "", "jpg": "",
"label": "برچسب", "label": "برچسب",
"loggable_type": "مورد ثبت",
"loggable_id": "شماره ثبت", "loggable_id": "شماره ثبت",
"loggable_type": "مورد ثبت",
"menu": "فهرست", "menu": "فهرست",
"minute(s)": "دقیقه", "minute(s)": "دقیقه",
"mobile": "موبایل", "mobile": "موبایل",
@ -594,15 +617,16 @@
"news": "خبر", "news": "خبر",
"not searchable": "غیرقابل جستجو", "not searchable": "غیرقابل جستجو",
"one second ago": "یک ثانیه پیش", "one second ago": "یک ثانیه پیش",
"password repeat": "تکرار گذرواژه", "paragraphs": "بند‌ها",
"parent_id": "والد", "parent_id": "والد",
"password repeat": "تکرار گذرواژه",
"payment success": "پرداخت موفق بود",
"pending": "معلق", "pending": "معلق",
"post": "نوشته", "post": "نوشته",
"price": "مبلغ", "price": "مبلغ",
"prop": "ویژگی",
"product": "محصول", "product": "محصول",
"product_id": "محصول", "product_id": "محصول",
"payment success": "پرداخت موفق بود", "prop": "ویژگی",
"rate": "امتیاز", "rate": "امتیاز",
"rateable_id": "مورد امتیاز", "rateable_id": "مورد امتیاز",
"rateable_type": " نوع امتیاز", "rateable_type": " نوع امتیاز",
@ -617,6 +641,7 @@
"status": "وضعیت", "status": "وضعیت",
"subject": "موضوع", "subject": "موضوع",
"subtitle": "زیرعنوان", "subtitle": "زیرعنوان",
"times": "مرتبه",
"title": "عنوان", "title": "عنوان",
"total_price": "مبلغ کل", "total_price": "مبلغ کل",
"transport": "حمل و نقل", "transport": "حمل و نقل",
@ -624,6 +649,7 @@
"user_id": "کاربر", "user_id": "کاربر",
"view": "بازدید", "view": "بازدید",
"webp": "", "webp": "",
"words": "واژگان",
"xShop": "", "xShop": "",
"yesterday": "دیروز" "yesterday": "دیروز"
} }

@ -381,3 +381,96 @@ a.btn,a.action-btn,a.circle-btn{
} }
#seo-hint{
.seo-report {
max-width: 100%;
margin: 15px 0;
background: #21252b;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.seo-score-container {
display: flex;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
gap: 20px;
}
.seo-score {
position: relative;
width: 80px;
height: 80px;
}
.seo-score-circle {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
color: white;
}
.seo-status {
flex: 1;
h3 {
margin: 0 0 5px 0;
font-size: 18px;
}
p {
margin: 0;
color: #eee;
}
}
.seo-details {
padding: 20px;
}
.seo-feedback {
margin-bottom: 20px;
ul {
margin: 10px 0;
padding-left: 20px;
}
li {
margin: 8px 0;
color: #ddd;
}
}
.seo-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.metric-card {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
h4 {
margin: 0 0 10px 0;
color: #333;
font-size: 14px;
}
}
.metric-value {
font-size: 16px;
font-weight: 500;
color: #2a2a2a;
}
.good {
background: #22c55e;
}
.average {
background: #f59e0b;
}
.poor {
background: #ef4444;
}
}

@ -152,7 +152,7 @@
<label for="body"> <label for="body">
{{__('Post Text')}} {{__('Post Text')}}
</label> </label>
<textarea name="body" class="ckeditorx form-control @error('body') is-invalid @enderror" <textarea name="body" class="ckeditorx seo-analyze form-control @error('body') is-invalid @enderror"
placeholder="{{__('Post Text')}}" placeholder="{{__('Post Text')}}"
rows="8">{{old('body',$item->body??null)}}</textarea> rows="8">{{old('body',$item->body??null)}}</textarea>
{{-- @trix(\App\Post::class, 'body')--}} {{-- @trix(\App\Post::class, 'body')--}}
@ -160,6 +160,19 @@
</div> </div>
</div> </div>
<div class="col-12">
<div class="form-group mt-3">
<label for="title">
{{__('Keyword')}} [{{__("SEO")}}]
</label>
<input name="keyword" type="text" id="keyword"
class="form-control @error('keyword') is-invalid @enderror"
placeholder="{{__('Keyword')}}" value="{{old('keyword',$item->keyword??null)}}"/>
</div>
<div id="seo-hint">
</div>
</div>
<div class="col-md-3 mt-3"> <div class="col-md-3 mt-3">
<div class="form-group"> <div class="form-group">
<label for="group_id"> <label for="group_id">

@ -110,10 +110,22 @@
<label for="description"> <label for="description">
{{__('Description Text')}} {{__('Description Text')}}
</label> </label>
<textarea name="desc" class="form-control ckeditorx @error('description') is-invalid @enderror" <textarea name="desc" class="form-control ckeditorx seo-analyze @error('description') is-invalid @enderror"
placeholder="{{__('Description Text')}}" placeholder="{{__('Description Text')}}"
id="description" id="description"
rows="8">{{old('description',$item->description??null)}}</textarea> rows="8">{{old('description',$item->description??null)}}</textarea>
</div> </div>
<div class="col-12">
<div class="form-group mt-3">
<label for="title">
{{__('Keyword')}} [{{__("SEO")}}]
</label>
<input name="keyword" type="text" id="keyword"
class="form-control @error('keyword') is-invalid @enderror"
placeholder="{{__('Keyword')}}" value="{{old('keyword',$item->keyword??null)}}"/>
</div>
<div id="seo-hint">
</div>
</div>
</div> </div>
</div> </div>

@ -6,5 +6,6 @@
var website_font = "{{gfx()['font']}}"; var website_font = "{{gfx()['font']}}";
window.routesList = @json(getAdminRoutes()); window.routesList = @json(getAdminRoutes());
</script> </script>
@include('components.translates')
</body> </body>
</html> </html>

@ -0,0 +1,29 @@
<script>
window.TR = {};
window.TR.contentShort = `{{__("Content is too short. Aim for at least 300 words")}}`;
window.TR.destinyLow = `{{__("Keyword density is too low")}}`;
window.TR.destinyHigh = `{{__("Keyword density is too high")}}`;
window.TR.shortKeyword = `{{__("Keyword is too short fix keyword to better analyze")}}`;
window.TR.keywordFirstParagraph = `{{__("Include keyword in the first paragraph")}}`;
window.TR.keywordHeading = `{{__("Include keyword in at least one heading")}}`;
window.TR.sentencesLong = `{{__("Sentences are too long")}}`;
window.TR.paragraphsLong = `{{__("Paragraphs are too long")}}`;
window.TR.sentencesComplex = `{{__("Too many complex sentences")}}`;
window.TR.paragraphAdd = `{{__("Add more paragraphs to improve structure")}}`;
window.TR.headingAdd = `{{__("Add headings to structure your content")}}`;
window.TR.useList = `{{__("Consider using lists to improve readability")}}`;
window.TR.good = `{{__("Good")}}`;
window.TR.averageNeeed = `{{__("Needs Improvement")}}`;
window.TR.poor = `{{__("Poor")}}`;
window.TR.basedOnKeyword = `{{__("Based on content analysis and keyword optimization")}}`;
window.TR.recommendations = `{{__("Recommendations")}}`;
window.TR.contentLength = `{{__("Content Length")}}`;
window.TR.keywordUsage = `{{__("Keyword Usage")}}`;
window.TR.avgSenLen = `{{__("Average Sentence Length")}}`;
window.TR.avgParaStruc = `{{__("Paragraph Structure")}}`;
window.TR.SEOScore = `{{__("SEO Score")}}`;
window.TR.words = `{{__("words")}}`;
window.TR.times = `{{__("times")}}`;
window.TR.paragraphs = `{{__("paragraphs")}}`;
</script>

@ -1,4 +1,5 @@
@cache('sitemap_attach'. cacheNumber(), 3600) @cache('sitemap_attach'. cacheNumber(), 3600)
{{--update every 1 hour--}}
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

@ -1,4 +1,5 @@
@cache('sitemap_clips'. cacheNumber(), 3600) @cache('sitemap_clips'. cacheNumber(), 3600)
{{--update every 1 hour--}}
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

@ -1,4 +1,5 @@
@cache('sitemap_gallery'. cacheNumber(), 3600) @cache('sitemap_gallery'. cacheNumber(), 3600)
{{--update every 1 hour--}}
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

@ -1,4 +1,5 @@
@cache('sitemap_products'. cacheNumber(), 3600) @cache('sitemap_products'. cacheNumber(), 3600)
{{--update every 1 hour--}}
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

@ -1,4 +1,5 @@
@cache('sitemap_post'. cacheNumber(), 3600) @cache('sitemap_post'. cacheNumber(), 3600)
{{--update every 1 hour--}}
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

@ -1,4 +1,5 @@
@cache('sitemap_products'. cacheNumber(), 3600) @cache('sitemap_products'. cacheNumber(), 3600)
{{--update every 1 hour--}}
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"

@ -1,4 +1,5 @@
@cache('sitemap'. cacheNumber(),43200) @cache('sitemap'. cacheNumber(),43200)
{{--update every 12 hours--}}
<sitemapindex xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <sitemapindex xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd"

Loading…
Cancel
Save