אלול

הצלחה ב-Redirects: איך לשמור על תנועה בכתובות URL חדשות ב-Moodle

במסגרת העברת אתר Moodle ישן עם גרסה 3.9 לאתר חדש עם גרסה 4.4, היה צורך להגדיר הפניות 301 עבור קישורים ישנים, כך שמשתמשים יוכלו להגיע לתכנים המתאימים באתר החדש. ההפניות הללו חשובות כדי למנוע שגיאות 404 ולעזור לשמור על חווית משתמש רציפה.

אחת הבעיות המרכזיות הייתה כיצד למצוא את כל המודולים – הפעילויות והמשאבים – והתכנים הישנים מהאתר הישן, ולהפנות אותם למודולים המתאימים באתר החדש. בייחוד, היה צורך למצוא את שמות המודולים והתכנים הקשורים אליהם, וליצור כתובות URL חדשות עבור כל אחד מהם באתר החדש.

לצורך כך, יצרתי מספר שאילתות שמחזירות את המידע הנחוץ על המודולים והקטעים באתר Moodle הישן. לאחר מכן, יצרתי סקריפט שמייצר קובץ הפניות 301 שאותו ניתן להכניס לקובץ מתאים בשרת האינטרנט.

שאילתות SQL

כדי להעביר את התכנים, היה צורך לאתר את כל המודולים והתכנים מהאתר הישן ולהתאים אותם לכתובות URL חדשות באתר החדש.

SELECT cs.id, section, NAME,  c.id AS Course_ID,c.shortname
FROM mdl_course_sections cs
INNER JOIN mdl_course c ON c.id=cs.course
WHERE course IN (139,160)
ORDER BY c.shortname, section;

השאילתה מחזירה את פרטי פרקי הקורס בטבלת mdl_course_sections עבור קורס עם מזהה 1843, תוך שילוב עם פרטי הקורס מטבלת mdl_course, וממיינת את התוצאות לפי שם הקורס הקצר ומספר הפרק.

את השאילה הזאת צריך להריץ גם ב-DB הישן וגם ב-DB החדש, כשכמובן מעדכנים את ה-id-ים של הקורסים בכל הרצה של שאילתה. כך נראה פלט לדוגמה:

ID Section NAME Course_ID Shortname
1 0 Introduction 1843 CS101
2 1 Week 1: Basics 1843 CS101
3 2 Week 2: Advanced Topics 1843 CS101

את הפלט של ה-DB הישן שומרים בקובץ old_courses_sections.csv ואת הפלט של ה-DB החדש שומרים בקובץ new_courses_sections.csv.

השאילתה השנייה מחזירה את שמות המודולים והקורסים הרלוונטיים, יחד עם שמות התכנים המיוחדים שנמצאים בכל מודול:

SELECT cm.id, m.name AS module_name, c.fullname AS course_name, cm.instance,
       CASE 
           WHEN m.name = 'assign' THEN (SELECT a.name FROM mdl_assign a WHERE a.id = cm.instance)
           WHEN m.name = 'quiz' THEN (SELECT q.name FROM mdl_quiz q WHERE q.id = cm.instance)
           WHEN m.name = 'url' THEN (SELECT u.name FROM mdl_url u WHERE u.id = cm.instance)
           WHEN m.name = 'book' THEN (SELECT b.name FROM mdl_book b WHERE b.id = cm.instance)
           WHEN m.name = 'page' THEN (SELECT p.name FROM mdl_page p WHERE p.id = cm.instance)
           WHEN m.name = 'forum' THEN (SELECT f.name FROM mdl_forum f WHERE f.id = cm.instance)
           WHEN m.name = 'hvp' THEN (SELECT h.name FROM mdl_hvp h WHERE h.id = cm.instance)
           WHEN m.name = 'questionnaire' THEN (SELECT q.name FROM mdl_questionnaire q WHERE q.id = cm.instance)
           WHEN m.name = 'choice' THEN (SELECT c.name FROM mdl_choice c WHERE c.id = cm.instance)
           WHEN m.name = 'folder' THEN (SELECT f.name FROM mdl_folder f WHERE f.id = cm.instance)
           WHEN m.name = 'resource' THEN (SELECT r.name FROM mdl_resource r WHERE r.id = cm.instance)
           ELSE NULL
       END AS module_content
FROM mdl_course_modules cm
JOIN mdl_modules m ON cm.module = m.id
JOIN mdl_course c ON cm.course = c.id
WHERE m.name != 'activequiz' AND c.id IN (139,160)
ORDER BY course_name, module_name, module_content;

השאילתה מחזירה לי את כל המודולים ותוכני המודול באתר Moodle לפי הקורסים שציינתי. אני מסננת את המודולים שאינם רלוונטיים כמו ‘activequiz’, ובאמצעות שימוש ב-CASE, אני מאחזרת את שמות התכנים לפי סוג המודול (לדוגמה, בחנים, מטלות, ספרים ועוד). לפי סוג המודול (m.name), מתבצעת שאילתה פנימית כדי לאחזר את שם התוכן של המודול מתוך הטבלה המתאימה, למשל:

  • אם המודול הוא ‘assign’, השם יילקח מטבלת mdl_assign
  • אם המודול הוא ‘quiz’, השם יילקח מטבלת mdl_quiz
  • וכן הלאה עבור סוגי המודולים השונים

גם את השאילה הזאת מריצים ב-DB הישן וב-DB בחדש, ושומרים כל אחד מהקבצים כ-old_courses_modules.csv וכ-new_courses_modules.csv בהתאמה. כך נראה פלט לדוגמה:

Module ID Module Name Course Name Instance ID Module Content
1001 assign Introduction to Programming 45 Assignment 1
1002 quiz Introduction to Programming 22 Midterm Exam
1003 url Data Structures 13 Reference Link
1004 book Advanced Algorithms 29 Chapter 1: Basics

יצירת הפניות 301

לאחר שאספתי את כל הנתונים הנחוצים בעזרת השאילתות, יצרתי סקריפט שמייצר קובץ הפניות 301. הסקריפט עובד כך שהוא מקשר בין כל כתובת URL ישנה לכתובת החדשה באתר Moodle החדש.

<?php
define('CLI_SCRIPT', true);

global $CFG;

require(__DIR__ . '/../../config.php');
require_once($CFG->libdir . '/clilib.php');

$old_module_csv_file = '/html/tmpCoursesBackup/old_courses_modules.csv';
$new_module_csv_file = '/html/tmpCoursesBackup/new_courses_modules.csv';
$old_section_csv_file = '/html/tmpCoursesBackup/old_courses_sections.csv';
$new_section_csv_file = '/html/tmpCoursesBackup/new_courses_sections.csv';
$module_output_file = '/html/tmpCoursesBackup/301_module_redirects.txt';
$section_output_file = '/html/tmpCoursesBackup/301_section_redirects.txt';
$html_file = '/html/tmpCoursesBackup/301_redirects.html';

$old_domain = 'https://example.ort.org.il';
$new_domain = 'https://example-new.ort.org.il';

function read_csv($file) {
    if (!file_exists($file)) {
        cli_error("Error: File does not exist: $file");
    }
    if (!is_readable($file)) {
        cli_error("Error: File is not readable: $file");
    }

    $data = [];
    $handle = @fopen($file, "r");
    if ($handle === FALSE) {
        cli_error("Error opening file: " . error_get_last()['message']);
    }
    while (($row = fgetcsv($handle, 1000, ",")) !== FALSE) {
        $data[] = $row;
    }
    fclose($handle);
    return $data;
}

function generate_module_redirects($old_data, $new_data) {
    global $old_domain, $new_domain;
    $redirects = [];

    foreach ($old_data as $i => $old_row) {
        if ($i === 0) continue; // Skip header row

        $new_row = $new_data[$i];

        // Extracting values
        list($old_module_id, $old_module_name, $old_course_name) = $old_row;
        list($new_module_id, $new_module_name, $new_course_name) = $new_row;

        // Module URL
        $redirects[] = "Redirect 301 $old_domain/mod/$old_module_name/view.php?id=$old_module_id $new_domain/mod/$new_module_name/view.php?id=$new_module_id";
    }

    return $redirects;
}

function generate_section_redirects($old_data, $new_data) {
    global $old_domain, $new_domain;
    $redirects = [];

    foreach ($old_data as $i => $old_row) {
        if ($i === 0) continue; // Skip header row
        list($old_section_id, $old_section_order, $old_section_name, $old_course_id, $old_course_shortname) = $old_row;
        list($new_section_id, $new_section_order, $new_section_name, $new_course_id, $new_course_shortname) = $new_data[$i];

        $redirects[] = "Redirect 301 $old_domain/course/view.php?id=$old_course_id&sectionid=$old_section_id $new_domain/course/view.php?id=$new_course_id&sectionid=$new_section_id";
        $redirects[] = "Redirect 301 $old_domain/course/view.php?id=$old_course_id&section=$old_section_order $new_domain/course/view.php?id=$new_course_id&section=$old_section_order";
    }

    return $redirects;
}

function read_redirects($file) {
    if (!file_exists($file)) {
        cli_error("Error: File does not exist: $file");
    }
    if (!is_readable($file)) {
        cli_error("Error: File is not readable: $file");
    }

    $data = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if ($data === FALSE) {
        cli_error("Error reading file: " . error_get_last()['message']);
    }
    return $data;
}

function generate_html($redirects) {
    $html = "<!DOCTYPE html>\n<html>\n<head>\n<title>301 Redirects</title>\n</head>\n<body>\n";
    $html .= "<h1>301 Redirects</h1>\n<ul>\n";

    foreach ($redirects as $redirect) {
        if (preg_match('/Redirect 301 (.+?) (.+)/', $redirect, $matches)) {
            $old_url = htmlspecialchars($matches[1]);
            $new_url = htmlspecialchars($matches[2]);
            $html .= "<li><a href=\"$old_url\">$old_url</a> &rarr; <a href=\"$new_url\">$new_url</a></li>\n";
        }
    }

    $html .= "</ul>\n</body>\n</html>";
    return $html;
}

// Main execution
cli_writeln("Reading old course module data from: $old_module_csv_file");
$old_module_data = read_csv($old_module_csv_file);

cli_writeln("Reading new course module data from: $new_module_csv_file");
$new_module_data = read_csv($new_module_csv_file);

if (count($old_module_data) !== count($new_module_data)) {
    cli_error("Error: The number of rows in old and new module CSV files do not match.");
}

cli_writeln("Generating 301 module redirects...");
$module_redirects = generate_module_redirects($old_module_data, $new_module_data);

// Write module redirects to file
cli_writeln("Writing 301 module redirects to: $module_output_file");
file_put_contents($module_output_file, implode("\n", $module_redirects));

cli_writeln("301 module redirects have been written to $module_output_file");

cli_writeln("Reading old course section data from: $old_section_csv_file");
$old_section_data = read_csv($old_section_csv_file);

cli_writeln("Reading new course section data from: $new_section_csv_file");
$new_section_data = read_csv($new_section_csv_file);

if (count($old_section_data) !== count($new_section_data)) {
    cli_error("Error: The number of rows in old and new section CSV files do not match.");
}

cli_writeln("Generating 301 section redirects...");
$section_redirects = generate_section_redirects($old_section_data, $new_section_data);

// Write section redirects to file
cli_writeln("Writing 301 section redirects to: $section_output_file");
file_put_contents($section_output_file, implode("\n", $section_redirects));

cli_writeln("301 section redirects have been written to $section_output_file");

// Combine module and section redirects
$all_redirects = array_merge($module_redirects, $section_redirects);

// Generate HTML file with clickable links
cli_writeln("Generating HTML file with clickable links...");
$html_content = generate_html($all_redirects);
file_put_contents($html_file, $html_content);

cli_writeln("HTML file with clickable links has been written to $html_file");
cli_writeln("Process completed successfully.");

מבנה הסקריפט:

הסקריפט מחולק למספר שלבים עיקריים:

  1. קריאת קבצי CSV – הפונקציה read_csv אחראית על קריאת הנתונים מקבצי ה-CSV ומחזירה אותם כמערך דו-ממדי. אם אחד הקבצים לא קיים או לא קריא, היא תציג הודעת שגיאה ותפסיק את הריצה.
  2. יצירת הפניות 301 עבור מודוליםהפונקציה generate_module_redirects יוצרת רשימת הפניות 301 עבור כל מודול ישן לעומת החדש. היא בודקת שכל שורה בקובץ הישן תואמת לשורה בקובץ החדש, ומשתמשת במזהי המודולים ליצירת כתובות ההפניה.
  3. יצירת הפניות 301 עבור קטעים (Sections) – בדומה למודולים, הפונקציה generate_section_redirects יוצרת הפניות 301 עבור הקטעים, תוך התייחסות למספרי הקטעים (sections) הישנים והחדשים.
  4. יצירת קובץ HTML – הפונקציה generate_html יוצרת קובץ HTML שמכיל רשימה של כל ההפניות בפורמט לחיץ. כל הפניה כוללת קישור ישן וקישור חדש.
  5. כתיבת הפניות לקבצים – הסקריפט יוצר שני קבצי טקסט, אחד עבור המודולים (301_module_redirects.txt) ואחד עבור הקטעים (301_section_redirects.txt), ומאחד את שניהם לקובץ HTML שמאפשר לראות את כל ההפניות בצורה מסודרת.

סיכום

בהעברת אתר Moodle מגרסה 3.9 לגרסה 4.4, היה צורך להקים מערכת הפניות 301 עבור קישורים ישנים, כדי להבטיח שימשיכו להיות גישה לתכנים המתאימים באתר החדש. השימוש בשאילתות SQL לשם הפקת נתונים על מודולים וקטעים מהאתר הישן והחדש, ושימוש בסקריפט ליצירת קובץ הפניות, אפשר להבטיח שמירה על חווית משתמש חלקה ומניעת שגיאות 404. ההנחות כוללות את יצירת הפניות מדויקות בין ה-URLs הישנים לחדשים, ולוודא שהקישורים מובילים למיקומים הנכונים. תהליך זה שומר על שלמות התוכן ומייעל את המעבר בין הגרסאות, מה שמסייע לשמור על שביעות רצון המשתמשים והמשכיות העבודה באתר.

מסך מחשב עם ממשק דפדפן אינטרנט המציג דף שגיאה או לולאה להפניה מחדש, עם קטעי קוד גלויים ושורת כתובת. כולל אלמנטים חזותיים כמו חצים ותרשימי זרימה המציינים את תהליך ההפניה מחדש, ואלמנטים נוספים כמו גלגלי שיניים וזכוכית מגדלת כדי לסמל פתרון בעיות ופתרון בעיות. מעל יושב הלוגו של Moodle

כתבו תגובה

כתובת הדוא"ל שלכם לא תוצג.