EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Web / เว็บสองภาษา 26 หน้า ทำไมต้องกลายเป็น .php แล้วลิงก์พังทั้งเว็บ
Web

เว็บสองภาษา 26 หน้า ทำไมต้องกลายเป็น .php แล้วลิงก์พังทั้งเว็บ

โดย Ton-Or ·19 ต.ค. 2026·อ่าน 9 นาที
เว็บสองภาษา 26 หน้า ทำไมต้องกลายเป็น .php แล้วลิงก์พังทั้งเว็บ

เว็บ static ที่มีแค่ 26 หน้า ฟังดูเป็นงานเล็กจนไม่น่ามีอะไรผิดพลาด จนกระทั่งวันที่ต้องแก้ข้อความในส่วนท้ายเว็บ แล้วพบว่าต้องเปิดแก้ทีละไฟล์ 26 รอบ ทางออกคือดึงส่วนที่ซ้ำออกมาเป็นไฟล์เดียว ซึ่งพาเว็บทั้งเว็บเข้าสู่ปัญหาที่ต่อกันเป็นทอด ๆ อีกสามชั้น

พอดีผมดูแลเว็บแนะนำสถานที่ท่องเที่ยวของชุมชนแห่งหนึ่ง เป็นเว็บ static ล้วน ๆ 13 หน้าภาษาไทย และอีก 13 หน้าภาษาอังกฤษที่ต้องตรงกันหนึ่งต่อหนึ่ง รวม 26 หน้า ตอนแรกไม่มีอะไรซับซ้อน จนวันที่ต้องแก้ข้อความในส่วนท้ายเว็บแล้วต้องไล่เปิด 26 ไฟล์ ผมเลยดึงส่วนท้ายกับส่วนหัวออกมาเป็นไฟล์เดียวแล้วให้ทุกหน้า include เอา ซึ่งพาเข้าปัญหาต่อกันสามชั้นรวด และปัญหาชั้นที่สามคือชั้นที่กินเวลาที่สุด

ชื่อชุมชน ชื่อหน้า และโดเมนในบทความนี้ผมเปลี่ยนใหม่หมดแล้ว แต่โครงไฟล์ กฎ rewrite และสคริปต์คือของจริงที่ใช้อยู่

ชั้นที่หนึ่ง: หน้าเว็บโชว์แท็ก PHP เป็นตัวหนังสือ

ผมเขียน include เสร็จ ทดสอบในเครื่องผ่านฉลุย พอเอาขึ้นเซิร์ฟเวอร์จริงกลับได้หน้าที่มีตัวหนังสือแปลก ๆ อยู่กลางจอ

$ curl -s https://example.com/index.html | tail -4
<?php $base = ""; include "includes/footer-th.php"; ?>
  </body>
</html>

โค้ด PHP ถูกส่งออกมาเป็นเนื้อหาดิบให้เบราว์เซอร์แสดง แปลว่าไฟล์นั้นไม่เคยผ่านตัวแปลภาษา PHP เลย เหตุผลตรงไปตรงมาคือโปรดักชันเป็น nginx ที่ตั้งไว้ว่านามสกุล .php เท่านั้นถึงจะส่งต่อให้ PHP-FPM ส่วน .html คือไฟล์ธรรมดา อ่านแล้วส่งออกไปตามนั้น

# nginx — กติกาที่ทำให้ .html ไม่มีวันได้เจอ PHP
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php-fpm.sock;     # เฉพาะ .php เท่านั้นที่มาถึงตรงนี้
}
location / {
    try_files $uri $uri/ =404;               # .html ถูกอ่านแล้วส่งออกไปตรง ๆ
}

ที่ในเครื่องผมใช้ได้เพราะเครื่องผมเป็น Apache ที่เผลอตั้ง AddHandler ให้ .html วิ่งผ่าน PHP ไว้ตั้งแต่งานก่อน ซึ่งเป็นการตั้งค่าที่ผมไม่แนะนำให้ใช้ในโปรดักชันด้วยซ้ำ เพราะมันบังคับให้ไฟล์ static ทุกไฟล์ต้องผ่านตัวแปลภาษา เสียทรัพยากรฟรี ๆ กับหน้าที่ไม่มีโค้ดสักบรรทัด

เครื่องพัฒนากับโปรดักชันคนละเว็บเซิร์ฟเวอร์ คือรอระเบิดเวลา

บั๊กชุดนี้ทั้งชุดเกิดจากข้อเดียวคือเครื่องผมเป็น Apache แต่ปลายทางเป็น nginx ผลคือทุกอย่างที่พึ่งการตั้งค่าของเว็บเซิร์ฟเวอร์ จะผ่านในเครื่องและพังบนของจริงเสมอ ทั้ง handler ของนามสกุลไฟล์ ทั้งกฎ rewrite ทั้ง .htaccess ที่ nginx ไม่อ่านเลยแม้แต่บรรทัดเดียว ถ้าจัดเครื่องให้ตรงกันไม่ได้ อย่างน้อยต้องมีตัวตรวจที่ยิงไปที่ URL จริงหลัง deploy ทุกครั้ง

ทางแก้ที่ถูกคือเปลี่ยนนามสกุลหน้าที่มี include ให้เป็น .php ไปเลย และตั้งกฎไว้ในใจว่า หน้าใหม่สร้างเป็น .php เสมอ ไม่ต้องมาลุ้นว่าหน้าไหนมีโค้ดหรือไม่มี

ชั้นที่สอง: เปลี่ยนนามสกุลแล้วลิงก์พังทั้งเว็บ

พอเปลี่ยนชื่อไฟล์ 26 ไฟล์ ลิงก์ภายในทั้งหมดที่ชี้ไป .html ก็ตายทันทีทั้งเว็บ ทั้งเมนู ทั้งลิงก์สลับภาษา ทั้งลิงก์ในเนื้อหา งานนี้แก้ด้วยมือไม่ไหวและแก้ด้วยมือแล้วจะตกหล่นแน่นอน ผมเลยเขียนสคริปต์ตัวเดียวที่ทำทั้งสองอย่างพร้อมกันคือแก้ลิงก์ก่อน แล้วค่อยเปลี่ยนชื่อไฟล์

# tools/rename_to_php.py — แก้ลิงก์ภายในก่อน แล้วค่อยเปลี่ยนนามสกุล ทำทีเดียวทั้ง 26 หน้า
import re
from pathlib import Path

ROOT = Path('site')
PAGES = [
    'index.html', 'sitemap.html', 'plan/one-day-trip.html',
    'stories/temple.html', 'stories/pond.html', 'stories/shrine.html',
    'stories/indigo.html', 'stories/festival.html', 'stories/craft.html',
    'stories/market.html', 'stories/landmark.html', 'stories/legend.html',
    'stories/museum.html',
]
PAGES += ['en/' + p for p in PAGES]          # ฝั่ง EN โครงเดียวกันเป๊ะ

# แก้เฉพาะ href/src ที่เป็นลิงก์ภายใน — ข้าม http, //, mailto:, tel: และ anchor
LINK = re.compile(
    r'((?:href|src)\s*=\s*["\'])(?!https?://|//|mailto:|tel:|#)([^"\']*?)\.html((?:[#?][^"\']*)?["\'])')

changed = 0
for rel in PAGES:
    src = ROOT / rel
    if not src.exists():
        print(f'[WARN] ไม่พบ {rel}')
        continue
    text = src.read_text(encoding='utf-8')
    new = LINK.sub(lambda m: f'{m.group(1)}{m.group(2)}.php{m.group(3)}', text)
    if new != text:
        src.write_text(new, encoding='utf-8')
        changed += 1
    src.rename(src.with_suffix('.php'))
    print(f'  {rel} -> {rel[:-5]}.php')

print(f'[OK] เปลี่ยนชื่อ {len(PAGES)} ไฟล์ · แก้ลิงก์ภายใน {changed} ไฟล์')

จุดที่ต้องระวังที่สุดในสคริปต์นี้คือ negative lookahead ที่กัน https?:// ไว้ ถ้าลืมใส่ ลิงก์ออกนอกเว็บที่บังเอิญลงท้ายด้วย .html จะถูกแก้ไปด้วย แล้วเราจะได้ลิงก์เสียที่ชี้ไปเว็บคนอื่นโดยไม่รู้ตัว ซึ่งหาเจอยากมากเพราะมันไม่พังในเว็บเรา

กติกา $base: แปดกรณีที่ต้องไม่หลุด

พอมี include แล้ว ปัญหาถัดมาคือลิงก์ที่อยู่ ข้างใน ไฟล์ include เอง ไฟล์ท้ายเว็บตัวเดียวถูกเรียกจากหน้าที่อยู่คนละชั้นกัน ลิงก์แบบสัมพัทธ์ในนั้นเลยชี้ไปคนละที่กันทุกครั้ง ผมแก้ด้วยการให้หน้าที่เรียกเป็นคนบอกว่าตัวเองอยู่ลึกแค่ไหน

<?php
// หน้าในโฟลเดอร์ stories/ ของฝั่งไทย
// $base = ทางกลับไปที่ "รากของภาษา" ใช้สำหรับลิงก์ข้างในไฟล์ include
// ส่วน path ของ include = ทางกลับไปที่ "รากของเว็บ" ที่โฟลเดอร์ includes/ อยู่
$base = "../";
include "../includes/footer-th.php";
?>
<?php // includes/footer-th.php — ใช้ $base ที่หน้าเรียกตั้งมาให้ ?>
<footer class="site-foot">
  <nav>
    <a href="<?= $base ?>">หน้าแรก</a>
    <a href="<?= $base ?>sitemap.php">แผนผังเว็บ</a>
    <a href="<?= $base ?>plan/one-day-trip.php">แผนเที่ยวหนึ่งวัน</a>
    <a href="<?= $base ?>en/" hreflang="en">English</a>
  </nav>
  <?php include __DIR__ . '/social-icons.php'; ?>
</footer>

สองค่านี้ไม่เท่ากันและนี่คือจุดที่ผมพลาดรอบแรก ผมนึกว่าใช้ค่าเดียวกันได้ ผลคือหน้าในโฟลเดอร์ย่อยของฝั่งอังกฤษลิงก์กลับไปหาฝั่งไทยหมด ผมเลยเขียนตารางแปดกรณีติดไว้ที่หัวไฟล์ include เลย

ตำแหน่งหน้า                    $base    path ของ include
/index.php                     ""       includes/footer-th.php
/sitemap.php                   ""       includes/footer-th.php
/stories/temple.php            "../"    ../includes/footer-th.php
/plan/one-day-trip.php         "../"    ../includes/footer-th.php
/en/index.php                  ""       ../includes/footer-en.php
/en/sitemap.php                ""       ../includes/footer-en.php
/en/stories/temple.php         "../"    ../../includes/footer-en.php
/en/plan/one-day-trip.php      "../"    ../../includes/footer-en.php

กฎอ่านง่าย ๆ สองข้อ
  $base          นับจากหน้านั้นกลับไปที่รากของ "ภาษา" ตัวเอง (/ หรือ /en/)
  path ของ include  นับจากหน้านั้นกลับไปที่รากของ "เว็บ" ที่ includes/ วางอยู่
ลิงก์ข้ามภาษาในฝั่ง EN ใช้ <?= $base ?>../ คือกลับไปรากภาษาแล้วขึ้นอีกชั้น
ใช้ DIR สำหรับ include ซ้อน แต่ใช้ base สำหรับลิงก์

ไฟล์ include ที่ไป include ต่ออีกชั้น ให้ใช้ __DIR__ เสมอ เพราะมันคือตำแหน่งจริงของไฟล์นั้นบนดิสก์ ไม่ขึ้นกับว่าใครเรียก ส่วน $base ใช้กับ ลิงก์ที่เบราว์เซอร์จะเดินตาม เท่านั้น สองอย่างนี้คนละระบบพิกัดกัน เอามาปนกันเมื่อไหร่จะได้ทั้งหน้าพังและ include ไม่เจอไฟล์พร้อมกัน

ชั้นที่สาม: clean URL ที่ rewrite กับ 301 ทำครึ่งเดียว

พอทุกหน้าเป็น .php ลูกค้าก็ทักมาว่า URL มีนามสกุลติดมาด้วยดูไม่สวย ซึ่งก็จริงและมีผลกับการทำ SEO ด้วย ผมเลยทำ clean URL ที่ Apache ฝั่ง DirectAdmin ซึ่งอ่าน .htaccess ให้

ตอนแรกผมเขียนแค่กฎเดียว คือถ้า URL ที่ขอมาไม่มีไฟล์อยู่จริง ให้ลองเติม .php ดู

# เวอร์ชันแรก — ทำครึ่งเดียว
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.*)$ $1.php [L]

ผลคือ /stories/temple ใช้ได้จริง แต่ลิงก์ในเมนูทั้งเว็บยังชี้ไป /stories/temple.php อยู่ดี กลายเป็นว่าทุกหน้าเข้าถึงได้สองทาง ซึ่งเป็นเนื้อหาซ้ำในสายตาเครื่องมือค้นหา ผมเลยเติมกฎ 301 เข้าไปอีกข้อ แล้วเว็บก็ตายทันที

# เวอร์ชันที่ทำเว็บตาย — วนไม่รู้จบ
RewriteRule ^(.*)\.php$ /$1 [R=301,L]
$ curl -sI https://example.com/stories/temple | head -3
HTTP/2 500
# ใน error log: Request exceeded the limit of 10 internal redirects

สาเหตุคือกฎบนสุดแปลง /stories/temple เป็น /stories/temple.php ภายในเซิร์ฟเวอร์ แล้ว Apache เอาผลลัพธ์นั้นวนกลับมาเข้ากฎอีกรอบ กฎ 301 เห็นว่าลงท้ายด้วย .php ก็เด้งกลับเป็น /stories/temple วนไปแบบนี้จนชนเพดาน ทางแก้คือกฎ 301 ต้องดูจาก สิ่งที่ผู้ใช้พิมพ์เข้ามาจริง ไม่ใช่สิ่งที่เซิร์ฟเวอร์แปลงไปแล้ว ตัวแปรที่เก็บของจริงไว้คือ THE_REQUEST

ทำไม rewrite กับ 301 รุ่นแรกถึงวนไม่รู้จบผู้ใช้ขอ/stories/templeกฎเติม .phprewrite ภายในกฎ 301 รุ่นแรกเห็น .php แล้วเด้งกลับวนไม่รู้จบ จนชนเพดาน 10 ครั้งแล้วได้ HTTP 500ทางแก้ THE_REQUEST ดูเฉพาะที่ผู้ใช้พิมพ์จริง ไม่ดูผลของ rewrite ภายใน
Apache เอาผลของกฎเติม .php วนกลับเข้ากฎอีกรอบ กฎ 301 รุ่นแรกเลยเห็น .php ที่เซิร์ฟเวอร์เติมเอง แล้วเด้งกลับเป็นหน้าสะอาด ซ้ำไปเรื่อย ๆ
# .htaccess ตัวที่ใช้จริง
AddDefaultCharset UTF-8
DirectoryIndex index.php index.html

<IfModule mod_rewrite.c>
  RewriteEngine on

  # 1) เด้ง .php ที่ผู้ใช้พิมพ์มาเอง ให้ไปที่ URL สะอาด (301 ครั้งเดียว)
  #    THE_REQUEST = บรรทัดคำขอดิบ เช่น "GET /stories/temple.php HTTP/1.1"
  #    ตัวแปรนี้ไม่เปลี่ยนตามการ rewrite ภายใน เลยไม่วน
  RewriteCond %{THE_REQUEST} \s/+(.+?)\.php[\s?] [NC]
  RewriteRule ^ /%1 [R=301,L]

  # 2) URL สะอาดที่ไม่มีไฟล์จริง ให้ลองเติม .php แล้วเสิร์ฟภายใน (ไม่เด้ง)
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteCond %{REQUEST_FILENAME}.php -f
  RewriteRule ^(.*)$ $1.php [L]
</IfModule>

<IfModule mod_headers.c>
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "SAMEORIGIN"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

ErrorDocument 404 /404.php
ลำดับของกฎสำคัญกว่าตัวกฎ

กฎเด้ง 301 ต้องอยู่ ก่อน กฎเติมนามสกุลเสมอ ถ้าสลับลำดับกัน คำขอที่เข้ามาแบบสะอาดจะถูกเติม .php ก่อน แล้วค่อยไปเจอกฎ 301 ที่พร้อมจะเด้งกลับ ต่อให้ใช้ THE_REQUEST ถูกแล้วก็ยังได้พฤติกรรมประหลาด และอย่าลืมว่า nginx ไม่อ่านไฟล์นี้เลย ถ้าปลายทางเป็น nginx ต้องไปเขียน try_files $uri $uri.php $uri/ =404; ในบล็อก server แทน

หลังแก้แล้วต้องตรวจสามเส้นทางให้ครบ ไม่ใช่ตรวจแค่ว่าหน้าเปิดได้

# 1) URL สะอาด ต้องได้ 200 ตรง ๆ ไม่เด้ง
$ curl -sI https://example.com/stories/temple | head -1
HTTP/2 200

# 2) URL เก่าที่มี .php ต้องเด้ง 301 ไปที่สะอาด "ครั้งเดียว"
$ curl -sI https://example.com/stories/temple.php | grep -E '^(HTTP|location)'
HTTP/2 301
location: /stories/temple

# 3) ตามไปจนสุดทาง ต้องเห็นการเด้งแค่หนึ่งครั้ง แล้วจบที่ 200
$ curl -sIL https://example.com/en/stories/temple.php | grep -c '^HTTP'
2

กับดักที่เหลือค้าง: ลิงก์ภายในกับ canonical

แก้กฎเสร็จแล้วยังไม่จบครับ เพราะลิงก์ในเมนูทั้งเว็บยังเป็น .php อยู่ ทุกครั้งที่ผู้ใช้กดเมนูเลยเสีย request ไปกับการเด้ง 301 หนึ่งรอบเปล่า ๆ และแท็ก canonical ที่ผมเขียนไว้ในทุกหน้าก็ยังประกาศตัวเองเป็น .php ซึ่งขัดกับสิ่งที่เซิร์ฟเวอร์บอก งานนี้ต้องไล่แก้อีกรอบทั้ง 26 หน้า

# tools/clean_links.py — ถอด .php ออกจากลิงก์ภายในและ canonical ทั้งเว็บ
import re
from pathlib import Path

ROOT = Path('site')
LINK = re.compile(r'((?:href|src)\s*=\s*["\'])(?!https?://|//|mailto:|tel:|#)([^"\']*?)\.php((?:[#?][^"\']*)?["\'])')
CANON = re.compile(r'(<link[^>]+rel=["\']canonical["\'][^>]+href=["\'][^"\']*?)\.php(["\'])')

n = 0
for p in sorted(ROOT.rglob('*.php')):
    if 'includes' in p.parts:
        continue                                  # ไฟล์ include ใช้ $base อยู่แล้ว อย่าไปแตะ
    t = p.read_text(encoding='utf-8')
    new = CANON.sub(r'\1\2', LINK.sub(lambda m: f'{m.group(1)}{m.group(2)}{m.group(3)}', t))
    # index.php ของแต่ละภาษาให้เหลือเป็น "./" กับ "../" ไม่ใช่ "index" โดด ๆ
    new = new.replace('href="index"', 'href="./"').replace('href="../index"', 'href="../"')
    if new != t:
        p.write_text(new, encoding='utf-8')
        n += 1
        print(f'  แก้ {p.relative_to(ROOT)}')

print(f'[OK] แก้ลิงก์ภายใน {n} ไฟล์')
ก่อนไล่แก้ลิงก์

ทำงานได้ แต่เสียเปล่าทุกคลิก

  • ทุกลิงก์ในเมนูเด้ง 301 หนึ่งรอบ
  • canonical ชี้ไป URL ที่ไม่อยากให้ใช้
  • เครื่องมือค้นหาเห็นสองที่อยู่ต่อหนึ่งหน้า
  • สถิติหน้าแตกเป็นสองรายการ
หลังไล่แก้

เส้นทางเดียว ทั้งคนและบอท

  • กดเมนูแล้วถึงปลายทางทันที
  • canonical ตรงกับสิ่งที่เซิร์ฟเวอร์บอก
  • เหลือที่อยู่เดียวต่อหนึ่งหน้า
  • 301 เหลือไว้สำหรับลิงก์เก่าข้างนอกเท่านั้น

ตัวเทียบ TH กับ EN ให้ตรงกันหนึ่งต่อหนึ่ง

เว็บสองภาษาที่แก้ด้วยสคริปต์แบบนี้มีความเสี่ยงเฉพาะตัวคือ ฝั่งหนึ่งถูกแก้แล้วอีกฝั่งตกหล่น แล้วไม่มีใครรู้จนกว่าจะมีคนต่างชาติเปิดเจอ ผมเลยเขียนตัวเทียบที่ไล่เช็กว่าองค์ประกอบสำคัญ 40 กว่าจุดยังมีอยู่ครบทั้งสองฝั่ง

# tools/compare_th_en.py — เทียบโครงสร้าง TH กับ EN ทีละจุด
from pathlib import Path

ROOT = Path('site')
MARKERS = [
    ('โลโก้',              'logo-site.png'),
    ('แบนเนอร์หลัก',        'hero-slide'),
    ('จุดวนแบนเนอร์',       'hero-slide-dots'),
    ('ไอคอนโซเชียล',        'social-icons.php'),
    ('ส่วนแนะนำชุมชน',      'id="about"'),
    ('ส่วนเรื่องเล่า',       'id="story"'),
    ('ส่วนสถานที่',         'id="attractions"'),
    ('การ์ดสถานที่ 9 ใบ',   'lg:grid-cols-3'),
    ('ส่วนแผนเที่ยว',       'id="itinerary"'),
    ('จุดเวลา 09:00',      '09:00'),
    ('จุดเวลา 15:15',      '15:15'),
    ('ส่วนของฝาก',          'id="shop"'),
    ('ส่วนติดต่อ',          'id="contact"'),
    ('ลิงก์แผนผังเว็บ',      'sitemap.php'),
    ('ลิงก์สลับภาษา',       'hreflang='),
    ('แท็ก canonical',     'rel="canonical"'),
]

PAIRS = [('index.php', 'en/index.php'),
         ('sitemap.php', 'en/sitemap.php'),
         ('plan/one-day-trip.php', 'en/plan/one-day-trip.php')]

bad = 0
for th_rel, en_rel in PAIRS:
    th = (ROOT / th_rel).read_text(encoding='utf-8')
    en = (ROOT / en_rel).read_text(encoding='utf-8')
    print(f'--- {th_rel}  vs  {en_rel}')
    for label, mark in MARKERS:
        a, b = th.count(mark), en.count(mark)
        if a != b:
            print(f'  [DIFF] {label}: TH={a} EN={b}  (marker: {mark})')
            bad += 1
    # ฝั่ง EN ต้องไม่หลงเหลือ include ของฝั่งไทย
    if 'footer-th.php' in en:
        print('  [ERR] ฝั่ง EN ยัง include footer ของไทยอยู่')
        bad += 1

print(f'\n[{"FAIL" if bad else "OK"}] จุดที่ไม่ตรงกัน {bad} จุด')

ตัวนี้ไม่ได้ฉลาดอะไรเลยครับ มันแค่นับจำนวนครั้งที่ marker ปรากฏในแต่ละฝั่งแล้วเทียบกัน แต่มันจับได้จริงทุกครั้งที่ผมแก้ฝั่งไทยแล้วลืมฝั่งอังกฤษ ซึ่งเกิดขึ้นบ่อยกว่าที่คิดมาก โดยเฉพาะตอนที่ลูกค้าขอเพิ่มอะไรด่วน ๆ

ตัวเช็กลิงก์เสียทั้งเว็บ ก่อนบอกว่าเสร็จ

ด่านสุดท้ายคือไล่ทุกลิงก์ในทุกหน้า แล้วยิงจริงไปที่ URL ปลายทาง ดูว่าได้ 200 หรือเปล่า และที่สำคัญคือดูว่ามีการเด้งกี่ครั้งก่อนถึงปลายทาง

# tools/check_links.py — ไล่ทุกลิงก์ภายใน ต้อง 200 และต้องไม่เด้งเกินหนึ่งครั้ง
import re, sys, urllib.parse, urllib.request
from pathlib import Path

BASE = 'https://example.com'
ROOT = Path('site')
HREF = re.compile(r'href=["\'](?!https?://|//|mailto:|tel:|#)([^"\']+)["\']')

seen, bad = set(), 0
for p in sorted(ROOT.rglob('*.php')):
    if 'includes' in p.parts:
        continue
    page_dir = '/' + p.relative_to(ROOT).parent.as_posix().strip('.').strip('/')
    for href in HREF.findall(p.read_text(encoding='utf-8')):
        url = urllib.parse.urljoin(BASE + page_dir.rstrip('/') + '/', href.split('#')[0])
        if not url or url in seen:
            continue
        seen.add(url)
        req = urllib.request.Request(url, method='HEAD',
                                     headers={'User-Agent': 'link-check/1.0'})
        try:
            with urllib.request.urlopen(req, timeout=10) as r:
                code = r.status
                final = r.geturl()          # urlopen ตามการเด้งให้เอง ถ้าปลายทางไม่ตรงกับที่ขอ = มีเด้ง
        except Exception as e:
            print(f'[ERR] {url}  <- {p.name}  ({e})')
            bad += 1
            continue
        if code != 200:
            print(f'[ERR] {code} {url}  <- {p.name}')
            bad += 1
        elif final != url:
            print(f'[WARN] เด้ง {url} -> {final}  <- {p.name}  (ควรแก้ลิงก์ต้นทาง)')
            bad += 1

print(f'\n[{"FAIL" if bad else "OK"}] ตรวจ {len(seen)} ลิงก์ · ปัญหา {bad}')
sys.exit(1 if bad else 0)

บรรทัดที่เตือนเรื่องการเด้งคือบรรทัดที่มีค่าที่สุดครับ เพราะลิงก์ที่เด้งยังใช้งานได้ปกติ ไม่มีใครแจ้งว่าพัง แต่มันคือร่องรอยว่าเรายังไล่แก้ไม่ครบ ถ้าปล่อยไว้นานพอ ลิงก์พวกนี้จะถูกก๊อปไปวางในที่อื่น ๆ แล้วกลายเป็นหนี้ที่ต้องแบก 301 ไว้ตลอดกาล

ตัวเลขกับลำดับที่ใช้จริง

26 หน้าจำนวนหน้าทั้งสองภาษา
3 ไฟล์ส่วนที่ดึงออกมาใช้ร่วม
8 กรณีตารางค่า base ที่ต้องถูก
2 กฎบรรทัดหลักใน htaccess
10เพดานเด้งวนของ Apache
1 ครั้งจำนวนเด้งที่ยอมรับได้
16 จุดmarker ที่ใช้เทียบสองภาษา
4 ตัวสคริปต์ที่เก็บไว้ใช้ซ้ำ
ลำดับการทำทั้งงานดึงส่วนซ้ำเป็น include3 ไฟล์->เปลี่ยนเป็น .phpพร้อมแก้ลิงก์->วางกติกา base8 กรณี->clean URL301 ก่อน rewrite->ไล่แก้ลิงก์ในเมนูให้เหลือเด้ง 0->เทียบสองภาษาแล้วเช็กลิงก์
  1. เช็กก่อนว่าโปรดักชันเป็นเว็บเซิร์ฟเวอร์อะไร แล้วตั้งเครื่องพัฒนาให้ตรงกัน อย่าพึ่ง handler แปลก ๆ ที่มีเฉพาะในเครื่องตัวเอง
  2. หน้าที่มี include ต้องเป็น .php และตั้งกฎว่าหน้าใหม่สร้างเป็น .php ไว้ก่อนเสมอ
  3. เปลี่ยนนามสกุลด้วยสคริปต์ที่แก้ลิงก์ภายในให้พร้อมกัน และต้องกันลิงก์ออกนอกเว็บไว้ด้วย lookahead
  4. เขียนตาราง $base ทุกกรณีติดไว้ที่หัวไฟล์ include อย่าไปจำเอาเอง
  5. ใน .htaccess วางกฎ 301 ที่อ่านจาก THE_REQUEST ไว้ก่อนกฎเติมนามสกุลเสมอ
  6. ไล่ถอด .php ออกจากลิงก์ภายในและ canonical ทั้งเว็บ ให้เหลือเด้งเป็นศูนย์สำหรับคนที่เดินตามเมนู
  7. รันตัวเทียบสองภาษาและตัวเช็กลิงก์ทุกครั้งหลัง deploy ไม่ใช่เฉพาะรอบใหญ่

ส่วนตัวผมได้บทเรียนจากงานนี้ว่าเว็บ static ไม่ได้ง่ายกว่าเว็บที่มีระบบหลังบ้านเสมอไป มันแค่ย้ายความซับซ้อนไปไว้ที่การตั้งค่าเว็บเซิร์ฟเวอร์กับวินัยในการแก้ไฟล์แทน และสองอย่างนี้ไม่มีใครเขียนเทสต์ให้ ต้องเขียนเอง งานที่ดูเป็นแค่การเปลี่ยนนามสกุลไฟล์ 26 ไฟล์ สุดท้ายแตะทั้งเว็บเซิร์ฟเวอร์ ทั้งโครงลิงก์ ทั้ง SEO ใครจะเอา .htaccess ชุดนี้ไปใช้ก็ปรับชื่อโฟลเดอร์กับหน้า 404 ให้ตรงกับงานตัวเองได้ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย