منصة مصطفى ووردبريس
أهلًا بيك، تشخيص قبل التنفيذ

كود خصم Hostinger انقر للنسخ 20% خصم على استضافة Hostinger الجديدة 10% عند كل تجديد التفاصيل

تخطَّ إلى المحتوى
منصة مصطفى ووردبريس
أدوات ومنصات وموارد

ربط Google Sheets مع WordPress REST API بأمان 2026: استيراد المقالات تلقائيًا

دليل آمن لاستيراد وتحديث مقالات WordPress من Google Sheets باستخدام Apps Script وREST API: Application Passwords، Script Properties، Draft-by-default، Categories/Tags، الصور، Logging وجدولة المزامنة.

شارك:
واتساب X فيسبوك لينكدإن تيليجرام
استيراد المنشورات تلقائيًا من Google Sheets إلى ووردبريس

استيراد المقالات من Google Sheets إلى WordPress مفيد عندما يدير فريق التحرير Briefs أوDrafts داخل Sheet ويريد تحويل الصفوف إلى Posts بصورة منظمة. لكن الربط يجب ألا يضع كلمة مرور WordPress الأساسية أوApplication Password داخل الخلايا أوSource code، وألا ينشر كل صف تلقائيًا بلا Approval gate.

الخلاصة: استخدم WordPress REST API عبر HTTPS مع Application Password مخصصة، خزّن الـcredential في Apps Script Properties بدل الكود، اجعل الحالة الافتراضية draft، واستخدم عمود approved كحاجز قبل publish. اختبر على Staging أولًا.

Architecture الربط

المسار سيكون:

Google Sheets → Apps Script → HTTPS → WordPress REST API → Post/Media

WordPress توفر endpoints رسمية مثل:

  • POST /wp-json/wp/v2/posts لإنشاء Post.
  • POST /wp-json/wp/v2/posts/<id> للتحديث.
  • POST /wp-json/wp/v2/media لرفع Media.

لماذا Application Password وليست كلمة مرور الحساب؟

WordPress تدعم Application Passwords رسميًا للمصادقة على REST API. ميزتها:

  • مخصصة لتطبيق معين.
  • يمكن إلغاؤها منفردة.
  • لا تكشف كلمة مرور تسجيل الدخول الأساسية.
  • تُستخدم مع Basic Authentication عبر HTTPS.

أنشئ User بصلاحيات لا تتجاوز ما يحتاجه Workflow بدل استخدام Administrator إذا لم تكن الإدارة الكاملة مطلوبة.

المتطلبات

  • WordPress يعمل عبر HTTPS.
  • REST API متاحة.
  • User لديه Capability لإنشاء/تعديل Posts المطلوبة.
  • Application Password لهذا التكامل فقط.
  • Google Sheet.
  • Apps Script مرتبط بالSheet.

شكل Google Sheet المقترح

العمودالغرض
post_idID بعد الإنشاء؛ يحدد التحديث لاحقًا
titleالعنوان
contentHTML/Content
statusdraft / pending / publish
approvedyes للسماح بالنشر
categoriesTerm IDs مفصولة بفواصل
tagsTerm IDs مفصولة بفواصل
featured_imageURL موثوقة للصورة
last_syncآخر مزامنة ناجحة
sync_errorآخر Error

لماذا نستخدم IDs للتصنيفات والوسوم؟

WordPress Posts REST endpoint تقبل categories وtags كمعرفات Terms. استخدام IDs يقلل الالتباس الناتج من أسماء متشابهة أوSlugs عربية.

1. أنشئ Application Password

  1. ادخل WordPress Dashboard.
  2. Users → Profile أوEdit User.
  3. ابحث عن Application Passwords.
  4. اكتب اسمًا مثل Google Sheets Publisher.
  5. أنشئ Password واحفظها في Password manager مؤقتًا.

Application Password تظهر عند الإنشاء للاستخدام، ويمكن إلغاؤها لاحقًا دون تغيير Password الحساب.

2. افتح Apps Script

من Google Sheets:

Extensions → Apps Script

لا تكتب Credential مباشرة في أول الملف مثل:

const WP_APP_PASSWORD = 'abcd...'; // لا تفعل هذا في مشروع مشترك

3. خزّن الإعدادات في Script Properties

Google Apps Script توفر PropertiesService لتخزين Key/Value configuration مرتبطة بالمشروع.

من Project Settings أضف Script Properties:

Keyمثال
WP_BASEhttps://example.com
WP_USEReditor_username
WP_APP_PASSWORDApplication Password

مهم: Script Properties ليست Secret Vault مشفرة مخصصة للأسرار عالية الحساسية، لكنها أفضل من لصق credential داخل Source code/Sheet. قلّل من يملك Edit access للمشروع، واستخدم Credential محدودة وقابلة للإلغاء.

4. الكود الأساسي الآمن

const SHEET_NAME = 'Posts';

function getConfig_() {
  const props = PropertiesService.getScriptProperties();
  const config = {
    base: (props.getProperty('WP_BASE') || '').replace(/\/$/, ''),
    user: props.getProperty('WP_USER') || '',
    appPassword: props.getProperty('WP_APP_PASSWORD') || ''
  };

  if (!config.base.startsWith('https://')) {
    throw new Error('WP_BASE must use HTTPS');
  }
  if (!config.user || !config.appPassword) {
    throw new Error('Missing WordPress credentials in Script Properties');
  }
  return config;
}

function authHeaders_() {
  const c = getConfig_();
  const token = Utilities.base64Encode(`${c.user}:${c.appPassword}`);
  return {
    Authorization: `Basic ${token}`,
    'Content-Type': 'application/json'
  };
}

5. قائمة مخصصة داخل Google Sheets

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('WordPress')
    .addItem('مزامنة الصف المحدد', 'syncActiveRow')
    .addItem('مزامنة كل الصفوف', 'syncAllRows')
    .addToUi();
}

6. قراءة Headers بصورة مرنة

function headerMap_(sheet) {
  const headers = sheet
    .getRange(1, 1, 1, sheet.getLastColumn())
    .getValues()[0]
    .map(v => String(v).trim().toLowerCase());

  const required = [
    'post_id','title','content','status','approved',
    'categories','tags','featured_image','last_sync','sync_error'
  ];

  const map = {};
  required.forEach(name => {
    const index = headers.indexOf(name);
    if (index === -1) throw new Error(`Missing column: ${name}`);
    map[name] = index + 1;
  });
  return map;
}

7. Draft-by-default + Approval gate

حتى لو كتب أحدهم publish بالخطأ، لا نسمح بالنشر إلا عندما approved=yes.

function safeStatus_(requested, approved) {
  const status = String(requested || 'draft').toLowerCase();
  const ok = String(approved || '').toLowerCase() === 'yes';

  if (status === 'publish' && !ok) return 'draft';
  if (['draft','pending','publish','private'].includes(status)) return status;
  return 'draft';
}

8. تحويل IDs إلى Arrays

function ids_(value) {
  if (!value) return [];
  return String(value)
    .split(',')
    .map(v => parseInt(v.trim(), 10))
    .filter(Number.isInteger);
}

9. إرسال Request إلى WordPress

function wpRequest_(path, options = {}) {
  const c = getConfig_();
  const response = UrlFetchApp.fetch(`${c.base}${path}`, {
    method: options.method || 'get',
    headers: options.headers || authHeaders_(),
    payload: options.payload ? JSON.stringify(options.payload) : undefined,
    muteHttpExceptions: true,
    followRedirects: false
  });

  const code = response.getResponseCode();
  const text = response.getContentText();
  let json = null;
  try { json = JSON.parse(text); } catch (e) {}

  if (code < 200 || code >= 300) {
    const message = json && json.message ? json.message : text.slice(0, 300);
    throw new Error(`WordPress HTTP ${code}: ${message}`);
  }
  return json;
}

10. إنشاء أوتحديث Post

function upsertPost_(row) {
  const payload = {
    title: String(row.title || '').trim(),
    content: String(row.content || ''),
    status: safeStatus_(row.status, row.approved)
  };

  const categories = ids_(row.categories);
  const tags = ids_(row.tags);
  if (categories.length) payload.categories = categories;
  if (tags.length) payload.tags = tags;

  if (row.featured_image) {
    payload.featured_media = uploadImage_(String(row.featured_image));
  }

  const id = parseInt(row.post_id, 10);
  const path = Number.isInteger(id)
    ? `/wp-json/wp/v2/posts/${id}`
    : '/wp-json/wp/v2/posts';

  return wpRequest_(path, {
    method: 'post',
    payload
  });
}

11. رفع الصورة بطريقة أكثر أمانًا

لا تجعل Sheet تستورد أي URL مجهولة. في فريق حقيقي استخدم Domain allowlist للصور.

function uploadImage_(url) {
  if (!/^https:\/\//i.test(url)) {
    throw new Error('Featured image must use HTTPS');
  }

  const image = UrlFetchApp.fetch(url, {
    muteHttpExceptions: true,
    followRedirects: true
  });

  if (image.getResponseCode() !== 200) {
    throw new Error(`Image fetch failed: HTTP ${image.getResponseCode()}`);
  }

  const blob = image.getBlob();
  const type = blob.getContentType() || '';
  if (!type.startsWith('image/')) {
    throw new Error('featured_image is not an image');
  }

  const ext = type.split('/')[1].replace('jpeg', 'jpg');
  const filename = `sheet-${Date.now()}.${ext}`;
  const c = getConfig_();
  const token = Utilities.base64Encode(`${c.user}:${c.appPassword}`);

  const response = UrlFetchApp.fetch(`${c.base}/wp-json/wp/v2/media`, {
    method: 'post',
    headers: {
      Authorization: `Basic ${token}`,
      'Content-Disposition': `attachment; filename="${filename}"`,
      'Content-Type': type
    },
    payload: blob.getBytes(),
    muteHttpExceptions: true,
    followRedirects: false
  });

  const code = response.getResponseCode();
  const text = response.getContentText();
  let json = null;
  try { json = JSON.parse(text); } catch (e) {}

  if (code < 200 || code >= 300 || !json || !json.id) {
    throw new Error(`Media upload failed: HTTP ${code}`);
  }
  return json.id;
}

12. تحويل Row إلى Object

function rowObject_(sheet, rowNumber, map) {
  const values = sheet
    .getRange(rowNumber, 1, 1, sheet.getLastColumn())
    .getValues()[0];

  const row = {};
  Object.keys(map).forEach(key => {
    row[key] = values[map[key] - 1];
  });
  return row;
}

13. مزامنة صف واحد

function syncRow_(sheet, rowNumber) {
  if (rowNumber <= 1) return;

  const map = headerMap_(sheet);
  const row = rowObject_(sheet, rowNumber, map);

  if (!String(row.title || '').trim()) return;

  try {
    const post = upsertPost_(row);
    sheet.getRange(rowNumber, map.post_id).setValue(post.id);
    sheet.getRange(rowNumber, map.last_sync).setValue(new Date());
    sheet.getRange(rowNumber, map.sync_error).clearContent();
  } catch (error) {
    sheet.getRange(rowNumber, map.sync_error).setValue(String(error.message || error));
    throw error;
  }
}

14. مزامنة الصف المحدد

function syncActiveRow() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error(`Sheet not found: ${SHEET_NAME}`);
  syncRow_(sheet, sheet.getActiveRange().getRow());
}

15. مزامنة كل الصفوف مع Lock

استخدم Lock لمنع Trigger وUser من تشغيل Sync متزامنتين.

function syncAllRows() {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(5000)) {
    throw new Error('Another sync is already running');
  }

  try {
    const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
    if (!sheet) throw new Error(`Sheet not found: ${SHEET_NAME}`);

    for (let row = 2; row <= sheet.getLastRow(); row++) {
      try {
        syncRow_(sheet, row);
      } catch (e) {
        console.error(`Row ${row}: ${e.message}`);
      }
    }
  } finally {
    lock.releaseLock();
  }
}

16. لماذا لا نمسح Error بعد Failure؟

وجود عمود sync_error يجعل Sheet نفسها Audit surface. بدل فتح Apps Script logs لكل مشكلة، يعرف المحرر لماذا فشل الصف.

17. Scheduling

Apps Script تدعم Installable time-driven triggers. يمكنك إنشاء Trigger لتشغيل syncAllRows دوريًا.

لكن قبل الجدولة:

  1. اختبر يدويًا.
  2. تأكد أن Default status = draft.
  3. اختبر Duplicate prevention عبر post_id.
  4. راجع Quotas.
  5. تأكد أن Errors تظهر بوضوح.

18. لا تجعل Trigger تنشر كل ما في Sheet

أفضل Workflow:

Writer → Editor review → approved=yes → Sync → WordPress

حتى مع هذا، يمكن جعل status=pending بدل Publish إذا فريقك يريد مراجعة نهائية داخل WordPress.

19. ماذا يحدث لو post_id موجود؟

يتم إرسال POST إلى:

/wp-json/wp/v2/posts/{id}

وبذلك تتحول Sheet إلى مصدر تحديث لهذا الصف. لا تغيّر post_id يدويًا إلا إذا تعرف ما تفعل.

20. ماذا لو حُذف Post من WordPress؟

ستحصل على REST error عند محاولة التحديث. لا تنشئ Post جديدة تلقائيًا عند 404 بدون قرار، وإلا قد تنتج Duplicate URLs/Content. اكتب Error وراجع الحالة يدويًا.

21. Categories/Tags غير الموجودة

هذا الكود يتوقع IDs موجودة. إذا ID غير صالحة، WordPress قد ترفض Request.

إنشاء Terms تلقائيًا يحتاج Workflow منفصلًا وصلاحيات إضافية؛ لا أخلطه في Importer الأساسي.

REST Posts endpoint تستخدم featured_media كـMedia ID. لذلك يجب رفع الصورة أولًا إلى Media endpoint ثم تمرير ID الناتجة.

الأفضل داخل فريق كبير استخدام Media library IDs أوAsset pipeline بدل تحميل URL نفسها كل Sync، حتى لا تكرر الملفات.

23. منع تكرار الصور

النسخة المبسطة أعلاه سترفع الصورة مرة أخرى عند كل Update يحتوي URL. للإنتاج أضف عمود featured_media_id أوابحث عن Attachment موجودة قبل الرفع.

24. Least Privilege

لا تستخدم Administrator لمجرد أن الاتصال أسهل. User المناسبة قد تكون Author أوEditor حسب ما يجب أن تنشره وتعدله.

اختبر:

  • هل يحتاج تعديل Posts لكتاب آخرين؟
  • هل يحتاج Upload files؟
  • هل يحتاج Publish أمPending فقط؟

25. HTTPS شرط عملي مهم

WordPress توثق استخدام Application Passwords مع Basic Auth عبر HTTPS. لا ترسل Credential على HTTP.

26. لا تضع Credential في Google Sheet

أي شخص يستطيع قراءة Sheet قد يحصل عليها. كذلك لا تضعها في Comment داخل Source.

إذا المشروع حساس جدًا استخدم Secret management مناسبًا للبنية بدل اعتبار Script Properties خزنة تشفير كاملة.

27. ألغِ Application Password عند إنهاء التكامل

ميزة Application Passwords أنها قابلة للإلغاء منفردة. إذا توقف Workflow أوغادر عضو الفريق، Revocation أفضل من ترك Credential قديمة بلا استخدام.

28. Logging

سجل على الأقل:

  • row.
  • post_id.
  • timestamp.
  • HTTP status.
  • error message.

لكن لا تسجل Authorization header أوApplication Password.

29. Idempotency

post_id هو أبسط Idempotency key هنا. الصف الجديد يُنشأ مرة، ثم كل Sync لاحقة تحدث نفس Post.

لبيئة أكبر يمكن استخدام UUID خاص بالمحتوى وتخزينه في Meta registered for REST.

30. ماذا عن SEO Metadata؟

Rank Math/Yoast protected meta ليست بالضرورة قابلة للتحديث عبر Core Posts endpoint مباشرة. لا تفترض أن إضافة Columns مثل seo_title ستعمل.

استخدم API موثقة للإضافة أوCustom endpoint مصمم بصلاحيات واضحة.

31. Gutenberg Blocks

يمكنك إرسال Block markup داخل content إذا مصدر المحتوى يولد markup صحيحة. لكن أسهل Workflow تحريري غالبًا HTML نظيف أوBlocks معروفة ومختبرة.

32. Scheduled publishing

REST Posts endpoint تدعم Status مثل future مع Date مناسبة حسب الصلاحيات والبيانات. لكن لا تضف Scheduling قبل ضبط timezone وApproval process.

33. Staging أولًا

قبل تشغيل Sync على Production:

  1. Clone الموقع أوأنشئ Staging.
  2. استخدم Application Password منفصلة.
  3. جرب 3 صفوف.
  4. اختبر Arabic content.
  5. اختبر HTML.
  6. اختبر Categories.
  7. اختبر Image.
  8. اختبر Update.
  9. اختبر Failure.

34. Checklist إنتاجية

الفحصالحالة
HTTPS□
Dedicated Application Password□
Least-privilege user□
Credential خارج Source/Sheet□
Draft by default□
Approval column□
post_id محفوظ□
Error column□
Lock لمنع التزامن□
Staging test□
Backup قبل Bulk sync□

أخطاء شائعة

  • وضع Application Password داخل الكود.
  • استخدام Administrator.
  • السماح بـpublish لكل Row.
  • عدم حفظ post_id.
  • رفع نفس الصورة كل مرة.
  • عدم فحص HTTP status.
  • تشغيل Trigger قبل الاختبار.
  • محاولة تحديث SEO private meta عبر Core API بلا دعم.
  • استخدام HTTP بدل HTTPS.

أسئلة شائعة

هل WordPress REST API تدعم إنشاء المقالات رسميًا؟

نعم. Core endpoint لإنشاء Post هي POST /wp/v2/posts وتدعم Title وContent وStatus وCategories وTags وFeatured media وغيرها.

هل Application Password آمنة؟

هي أفضل من مشاركة Password الحساب للتكاملات، وقابلة للإلغاء منفردة، لكن يجب إرسالها عبر HTTPS وحمايتها مثل أي Credential.

هل Script Properties مشفرة؟

لا تتعامل معها كSecret Vault مشفرة. فائدتها الأساسية هنا عدم Hard-code البيانات في Source أوSheet وتحديد Scope التخزين. قيّد الوصول للمشروع.

هل يمكن النشر تلقائيًا؟

نعم إذا User لديها صلاحية Publish، لكن أنصح بجعل Draft/Pending هي Default مع Approval gate.

هل أحتاج Plugin في WordPress؟

لـPosts API الأساسية وApplication Passwords الحديثة لا تحتاج Plugin مصادقة إضافية عادة، طالما Core REST API تعمل وبيئتك لا تعطل الميزة.

الخلاصة

ربط Google Sheets مع WordPress مفيد عندما يُبنى كـContent pipeline منضبطة، وليس Script تنشر أي Row تجدها. استخدم REST API الرسمية، Application Password محدودة، HTTPS، Script Properties، Draft-by-default، Approval gate، Logging وpost_id لمنع التكرار. بهذه البنية يمكن للفريق إدارة عشرات أوحتى مئات Drafts بدون تحويل Sheet إلى نقطة خطر أوفوضى نشر.

مصادر رسمية

تقرأ الآن Architecture الربط
المحتويات
استفدت من المقال؟ شاركه مع شخص يحتاجه.
واتساب X فيسبوك لينكدإن تيليجرام
كتبه المدير التنفيذي للمنصة

مصطفى زكي، Senior WordPress Platform Engineer ومؤسس منصة مصطفى ووردبريس. متخصص في تطوير WordPress وWooCommerce، القوالب والوظائف المخصصة، الأداء، الأمان، وSEO/AEO، بمنهج يبدأ بالتشخيص والقياس قبل التنفيذ.

WordPress WooCommerce Technical SEO الأداء والأمان

أضف تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

تواصل واتساب